<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://samuelmullen.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://samuelmullen.com/" rel="alternate" type="text/html" /><updated>2026-04-21T02:13:02+00:00</updated><id>https://samuelmullen.com/feed.xml</id><title type="html">Samuel Mullen</title><subtitle>Write an awesome description for your new site here. You can edit this line in _config.yml. It will appear in your document head meta (for Google search results) and in your feed.xml site description.</subtitle><entry><title type="html">Asynchronous Tasks and Streaming UIs in Phoenix Liveview</title><link href="https://samuelmullen.com/articles/asynchronous-tasks-and-streaming-uis-in-phoenix-liveview" rel="alternate" type="text/html" title="Asynchronous Tasks and Streaming UIs in Phoenix Liveview" /><published>2025-12-29T05:00:00+00:00</published><updated>2025-12-29T05:00:00+00:00</updated><id>https://samuelmullen.com/articles/asynchronous-tasks-and-streaming-uis-in-phoenix-liveview</id><content type="html" xml:base="https://samuelmullen.com/articles/asynchronous-tasks-and-streaming-uis-in-phoenix-liveview"><![CDATA[<aside class="panel panel-default pull-right col-md-4">
<h3>For What It's Worth</h3>
<p>Sean's article was bleeding edge and about as simple as it could get when it
was written. The solutions described in this article weren't available until
five months after he wrote his.</p>
</aside>

<p>The <a href="https://thethreevirtues.com/">three great virtues of a programmer</a> are
laziness, impatience, and hubris. I lean toward laziness more than the other
two, often thinking, “There’s got to be an easier way.” A recent example of this
was when I was experimenting with streaming LLM output in LiveView. While
researching, I found that the most popular article on the topic, <a href="https://www.hackwithgpt.com/blog/streaming-chatgpt-responses-with-phoenix-liveview/">Streaming ChatGPT Responses With Phoenix LiveView</a>
by Sean Moriarity, to be quite complex. I knew there had to be a simpler
solution; that’s when I stumbled across LiveView’s asynchronous functions:
<code class="language-plaintext highlighter-rouge">assign_async/4</code>, <code class="language-plaintext highlighter-rouge">stream_async/4</code>, <code class="language-plaintext highlighter-rouge">start_async/4</code>, and <code class="language-plaintext highlighter-rouge">handle_async/3</code>.</p>

<p>These functions leverage Elixir’s lightweight processes to perform work off the
main LiveView process. When the work is completed, your application can deal
with the results without disrupting your user’s experience.</p>

<h2 id="use-cases">Use cases</h2>

<p>There are any number of reasons and situations where you might want to run
processes asynchronously in LiveView: presenting a dashboard immediately while
allowing charts and graphs to eventually load, performing multiple data fetches
concurrently, isolate the UI from high-latency or failure-prone operations,
presenting data as it becomes available, etc. Every use case boils down into
three general categories:</p>

<ul>
  <li><strong>Streaming data:</strong> Examples of this include LLM-style data output, displaying
log output, and live data feeds.</li>
  <li><strong>Long running processes:</strong> Running reports, progressive rendering, and
fire-and-forget-it style processes are all potential use cases.</li>
  <li><strong>Resilience and lifecycle management:</strong> Allows you to separate the retrieval
of data from its presentation.</li>
</ul>

<p>To demonstrate these use cases and how each of the four “async” functions can be
used to address the use case, we’ll use a simple LiveView module, rewriting it
for each example.</p>

<h3 id="assign_async4"><code class="language-plaintext highlighter-rouge">assign_async/4</code></h3>

<p><code class="language-plaintext highlighter-rouge">assign_async/4</code> is as straightforward as it gets, and once you’ve
seen how to use it, you’ll start using it all the time. It’s just like using
<code class="language-plaintext highlighter-rouge">assign/3</code>, but instead of assigning a value, you provide it with a function.
Upon completion, the function updates the assigned key with the result. In the
code below (line 28), we assign <code class="language-plaintext highlighter-rouge">async_output</code> the <code class="language-plaintext highlighter-rouge">simulate_work/0</code> function
which, when called, “sleeps” for two seconds, and then returns a success tuple
with key set to “Well that took a long time!”.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">MyAppWeb</span><span class="o">.</span><span class="no">PageLive</span> <span class="k">do</span>
  <span class="kn">use</span> <span class="no">MyAppWeb</span><span class="p">,</span> <span class="ss">:live_view</span>

  <span class="n">alias</span> <span class="no">Phoenix</span><span class="o">.</span><span class="no">LiveView</span><span class="o">.</span><span class="no">AsyncResult</span>

  <span class="nv">@impl</span> <span class="no">true</span>
  <span class="k">def</span> <span class="n">mount</span><span class="p">(</span><span class="n">_params</span><span class="p">,</span> <span class="n">_session</span><span class="p">,</span> <span class="n">socket</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">socket</span> <span class="o">=</span> <span class="n">assign</span><span class="p">(</span><span class="n">socket</span><span class="p">,</span> <span class="ss">:async_output</span><span class="p">,</span> <span class="no">AsyncResult</span><span class="o">.</span><span class="n">loading</span><span class="p">(</span><span class="no">false</span><span class="p">))</span>

    <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="n">socket</span><span class="p">}</span>
  <span class="k">end</span>

  <span class="nv">@impl</span> <span class="no">true</span>
  <span class="k">def</span> <span class="n">render</span><span class="p">(</span><span class="n">assigns</span><span class="p">)</span> <span class="k">do</span>
    <span class="sx">~H""</span><span class="s2">"
    &lt;button phx-click="</span><span class="n">output</span><span class="s2">" class="</span><span class="n">bg</span><span class="o">-</span><span class="n">blue</span><span class="o">-</span><span class="mi">600</span> <span class="n">text</span><span class="o">-</span><span class="n">white</span> <span class="n">py</span><span class="o">-</span><span class="mi">2</span> <span class="n">px</span><span class="o">-</span><span class="mi">4</span><span class="s2">"&gt;
      Async output!
    &lt;/button&gt;

    &lt;%= if @async_output.ok? do %&gt;
      &lt;p&gt;&lt;%= @async_output.result %&gt;&lt;/p&gt;
    &lt;% end %&gt;
    """</span>
  <span class="k">end</span>

  <span class="nv">@impl</span> <span class="no">true</span>
  <span class="k">def</span> <span class="n">handle_event</span><span class="p">(</span><span class="s2">"output"</span><span class="p">,</span> <span class="n">_params</span><span class="p">,</span> <span class="n">socket</span><span class="p">)</span> <span class="k">do</span>
    <span class="p">{</span><span class="ss">:noreply</span><span class="p">,</span> <span class="n">assign_async</span><span class="p">(</span><span class="n">socket</span><span class="p">,</span> <span class="ss">:async_output</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">simulate_work</span><span class="o">/</span><span class="mi">0</span><span class="p">)}</span>
  <span class="k">end</span>

  <span class="k">defp</span> <span class="n">simulate_work</span><span class="p">()</span> <span class="k">do</span>
    <span class="no">Process</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">2000</span><span class="p">)</span>
    <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="p">%{</span><span class="ss">async_output:</span> <span class="s2">"Well that took a long time!"</span><span class="p">}}</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>If you were to run the code above, you could click a button and then see “Well
that took a long time!” displayed below the button two seconds later.</p>

<p>One thing to note, which will also come into play in the other functions, is the
use of <code class="language-plaintext highlighter-rouge">Phoenix.LiveView.AsyncResult</code>. We use this module to keep track of a
key’s status. We initialize <code class="language-plaintext highlighter-rouge">AsyncResult</code> with <code class="language-plaintext highlighter-rouge">loading: false</code> to represent an
idle state. When the <code class="language-plaintext highlighter-rouge">simulate_work/0</code> function completes, it automatically
updates <code class="language-plaintext highlighter-rouge">AsyncResult</code> with an <code class="language-plaintext highlighter-rouge">ok</code> status.</p>

<blockquote>
  <p>Each key passed to <code class="language-plaintext highlighter-rouge">assign_async/3</code>; will be assigned to an
<code class="language-plaintext highlighter-rouge">Phoenix.LiveView.AsyncResult</code> struct holding the status of the operation and
the result when the function completes.</p>
</blockquote>

<h3 id="stream_async4"><code class="language-plaintext highlighter-rouge">stream_async/4</code></h3>

<p>In all honesty, I’ve struggled to figure out why <code class="language-plaintext highlighter-rouge">stream_async/4</code> exists. I
understand that it’s supposed to help with reducing the boilerplate of
<code class="language-plaintext highlighter-rouge">start_async/4</code> and <code class="language-plaintext highlighter-rouge">handle_async/3</code>, but it also appears to only work in the
<code class="language-plaintext highlighter-rouge">mount/3</code> function and it requires a specific data structure to integrate with
Phoenix streams. But for the sake of completeness…</p>

<p>Below is an example of using it to display a list of ten random words from the
Lorem Ipsum after a one-second delay.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">MyAppWeb</span><span class="o">.</span><span class="no">PageLive</span> <span class="k">do</span>
  <span class="kn">use</span> <span class="no">MyAppWeb</span><span class="p">,</span> <span class="ss">:live_view</span>

  <span class="n">alias</span> <span class="no">Phoenix</span><span class="o">.</span><span class="no">LiveView</span><span class="o">.</span><span class="no">AsyncResult</span>

  <span class="nv">@impl</span> <span class="no">true</span>
  <span class="k">def</span> <span class="n">mount</span><span class="p">(</span><span class="n">_params</span><span class="p">,</span> <span class="n">_session</span><span class="p">,</span> <span class="n">socket</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">socket</span> <span class="o">=</span>
      <span class="n">socket</span>
      <span class="o">|&gt;</span> <span class="n">stream_async</span><span class="p">(</span><span class="ss">:async_output</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">stream_response</span><span class="o">/</span><span class="mi">0</span><span class="p">)</span>

    <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="n">socket</span><span class="p">}</span>
  <span class="k">end</span>

  <span class="nv">@impl</span> <span class="no">true</span>
  <span class="k">def</span> <span class="n">render</span><span class="p">(</span><span class="n">assigns</span><span class="p">)</span> <span class="k">do</span>
    <span class="sx">~H""</span><span class="s2">"
    &lt;.async_result :let={async_output} assign={@async_output}&gt;
      &lt;:loading&gt;Loading output...&lt;/:loading&gt;
      &lt;:failed :let={_failure}&gt;there was an error loading the output&lt;/:failed&gt;
      &lt;%= if async_output do %&gt;
        &lt;ul&gt;
        &lt;%= for {x, item} &lt;- @streams.async_output do %&gt;
          &lt;li&gt;ID: &lt;%= x %&gt; - &lt;%= item.id %&gt; - &lt;%= item.word %&gt;&lt;/li&gt;
        &lt;% end %&gt;
        &lt;/ul&gt;
      &lt;% else %&gt;
        You don't have output yet.
      &lt;% end %&gt;
    &lt;/.async_result&gt;
    """</span>
  <span class="k">end</span>

  <span class="k">defp</span> <span class="n">stream_response</span><span class="p">()</span> <span class="k">do</span>
    <span class="no">Process</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">1000</span><span class="p">)</span>

    <span class="n">words</span> <span class="o">=</span>
      <span class="s2">"Lorem ipsum dolor sit amet, ullum phaedrum in est, sit viris dissentiunt eu. Ad qui aperiri senserit necessitatibus. In ferri persius vel, te option saperet pertinacia sit. At duis nulla zril per. Alienum accumsan qui ei, at quem constituto pri, ei facer libris cum. Doctus integre blandit pri an, quas intellegam quaerendum eu per."</span>
      <span class="o">|&gt;</span> <span class="no">String</span><span class="o">.</span><span class="n">split</span><span class="p">()</span>
      <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">take_random</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span>
      <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">with_index</span><span class="p">(</span><span class="k">fn</span> <span class="n">word</span><span class="p">,</span> <span class="n">idx</span> <span class="o">-&gt;</span> <span class="p">%{</span><span class="ss">id:</span> <span class="n">idx</span><span class="p">,</span> <span class="ss">word:</span> <span class="n">word</span><span class="p">}</span> <span class="k">end</span><span class="p">)</span>

    <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="n">words</span><span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<aside class="panel panel-default pull-right col-md-4">
<h3>In all fairness</h3>

<p>I spent way too much time trying to figure out `stream_async/4`, and my
negative attitude about the function may be the result of my annoyance at
that.</p>
</aside>

<p>It looks more complicated than what it is. The bulk of the work is in the stream
response which sleeps for one second (i.e. 1,000 milliseconds), and then pulls
10 random words from the Lorem Ipsum string which it returns as an <code class="language-plaintext highlighter-rouge">:ok</code> tuple.
The list of words is then rendered in the <code class="language-plaintext highlighter-rouge">async_result/1</code> block. We use
<code class="language-plaintext highlighter-rouge">async_result/1</code> for rendering, because it handles the potential errors that
<code class="language-plaintext highlighter-rouge">stream_async/4</code> might receive. If, for example, <code class="language-plaintext highlighter-rouge">stream_response/0</code> returns an
<code class="language-plaintext highlighter-rouge">:error</code> tuple, <code class="language-plaintext highlighter-rouge">async_result/1</code> would have rendered “there was an error loading
the output”.</p>

<p>This is an example what the above LiveView module will present after waiting one
second:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ID: async_output-0 - 0 - In
ID: async_output-1 - 1 - necessitatibus.
ID: async_output-2 - 2 - pri
ID: async_output-3 - 3 - aperiri
ID: async_output-4 - 4 - Ad
ID: async_output-5 - 5 - ullum
ID: async_output-6 - 6 - quaerendum
ID: async_output-7 - 7 - dissentiunt
ID: async_output-8 - 8 - intellegam
ID: async_output-9 - 9 - ei,
</code></pre></div></div>

<p>In my personal opinion, you’re better off skipping this function and using to
the next two. They’re much more flexible and provide finer control over what and
when you can perform asynchronous work.</p>

<h3 id="start_async4-and-handle_async4"><code class="language-plaintext highlighter-rouge">start_async/4</code> and <code class="language-plaintext highlighter-rouge">handle_async/4</code></h3>

<p>Anything you can do with <code class="language-plaintext highlighter-rouge">assign_async/4</code> and <code class="language-plaintext highlighter-rouge">stream_async/4</code>, you can do with
<code class="language-plaintext highlighter-rouge">start_async/4</code> and <code class="language-plaintext highlighter-rouge">handle_async/3</code>. In the example below, we’ll use the two
functions to stream random words to the page in the same way an LLM might. The
module creates a very simple LiveView page. It has a “Go!” button, which
triggers the word stream. A “Cancel” button which cancels the stream and is only
visible while words are streaming. Lastly, it has a “Reset” button to clear out
previously streamed words.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">MyAppWeb</span><span class="o">.</span><span class="no">PageLive</span> <span class="k">do</span>
  <span class="kn">use</span> <span class="no">MyAppWeb</span><span class="p">,</span> <span class="ss">:live_view</span>

  <span class="n">alias</span> <span class="no">Phoenix</span><span class="o">.</span><span class="no">LiveView</span><span class="o">.</span><span class="no">AsyncResult</span>

  <span class="nv">@impl</span> <span class="no">true</span>
  <span class="k">def</span> <span class="n">mount</span><span class="p">(</span><span class="n">_params</span><span class="p">,</span> <span class="n">_session</span><span class="p">,</span> <span class="n">socket</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">socket</span> <span class="o">=</span>
      <span class="n">socket</span>
      <span class="o">|&gt;</span> <span class="n">assign</span><span class="p">(</span><span class="ss">:lorem</span><span class="p">,</span> <span class="s2">""</span><span class="p">)</span>
      <span class="o">|&gt;</span> <span class="n">assign</span><span class="p">(</span><span class="ss">:async_state</span><span class="p">,</span> <span class="no">AsyncResult</span><span class="o">.</span><span class="n">loading</span><span class="p">(</span><span class="no">false</span><span class="p">))</span>

    <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="n">socket</span><span class="p">}</span>
  <span class="k">end</span>

  <span class="nv">@impl</span> <span class="no">true</span>
  <span class="k">def</span> <span class="n">render</span><span class="p">(</span><span class="n">assigns</span><span class="p">)</span> <span class="k">do</span>
    <span class="sx">~H""</span><span class="s2">"
    &lt;%= if @async_state.loading do %&gt;
      &lt;button phx-click="</span><span class="n">cancel</span><span class="s2">" class="</span><span class="n">bg</span><span class="o">-</span><span class="n">red</span><span class="o">-</span><span class="mi">800</span> <span class="n">text</span><span class="o">-</span><span class="n">white</span> <span class="n">py</span><span class="o">-</span><span class="mi">2</span> <span class="n">px</span><span class="o">-</span><span class="mi">4</span><span class="s2">"&gt;
        Cancel
      &lt;/button&gt;
    &lt;% else %&gt;
      &lt;button phx-click="</span><span class="n">go</span><span class="s2">" class="</span><span class="n">bg</span><span class="o">-</span><span class="n">blue</span><span class="o">-</span><span class="mi">600</span> <span class="n">text</span><span class="o">-</span><span class="n">white</span> <span class="n">py</span><span class="o">-</span><span class="mi">2</span> <span class="n">px</span><span class="o">-</span><span class="mi">4</span><span class="s2">"&gt;
        Go!
      &lt;/button&gt;
      &lt;button phx-click="</span><span class="n">reset</span><span class="s2">" class="</span><span class="n">bg</span><span class="o">-</span><span class="n">blue</span><span class="o">-</span><span class="mi">600</span> <span class="n">text</span><span class="o">-</span><span class="n">white</span> <span class="n">py</span><span class="o">-</span><span class="mi">2</span> <span class="n">px</span><span class="o">-</span><span class="mi">4</span><span class="s2">"&gt;
        Reset
      &lt;/button&gt;
    &lt;% end %&gt;

    &lt;br /&gt;

    {@lorem}
    """</span>
  <span class="k">end</span>

  <span class="nv">@impl</span> <span class="no">true</span>
  <span class="k">def</span> <span class="n">handle_event</span><span class="p">(</span><span class="s2">"go"</span><span class="p">,</span> <span class="n">_params</span><span class="p">,</span> <span class="n">socket</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">pid</span> <span class="o">=</span> <span class="n">self</span><span class="p">()</span>

    <span class="n">socket</span> <span class="o">=</span>
      <span class="n">socket</span>
      <span class="o">|&gt;</span> <span class="n">assign</span><span class="p">(</span><span class="ss">:async_state</span><span class="p">,</span> <span class="no">AsyncResult</span><span class="o">.</span><span class="n">loading</span><span class="p">())</span>
      <span class="o">|&gt;</span> <span class="n">start_async</span><span class="p">(</span><span class="ss">:data_stream</span><span class="p">,</span> <span class="k">fn</span> <span class="o">-&gt;</span>
        <span class="n">stream_response</span><span class="p">(</span><span class="n">pid</span><span class="p">)</span>
      <span class="k">end</span><span class="p">)</span>

    <span class="p">{</span><span class="ss">:noreply</span><span class="p">,</span> <span class="n">socket</span><span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">handle_event</span><span class="p">(</span><span class="s2">"reset"</span><span class="p">,</span> <span class="n">_params</span><span class="p">,</span> <span class="n">socket</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">socket</span> <span class="o">=</span>
      <span class="n">socket</span>
      <span class="o">|&gt;</span> <span class="n">assign</span><span class="p">(</span><span class="ss">:async_state</span><span class="p">,</span> <span class="no">AsyncResult</span><span class="o">.</span><span class="n">ok</span><span class="p">(</span><span class="s2">"Reset"</span><span class="p">))</span>
      <span class="o">|&gt;</span> <span class="n">assign</span><span class="p">(</span><span class="ss">:lorem</span><span class="p">,</span> <span class="s2">""</span><span class="p">)</span>

    <span class="p">{</span><span class="ss">:noreply</span><span class="p">,</span> <span class="n">socket</span><span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">handle_event</span><span class="p">(</span><span class="s2">"cancel"</span><span class="p">,</span> <span class="n">_params</span><span class="p">,</span> <span class="n">socket</span><span class="p">)</span> <span class="k">do</span>
    <span class="p">{</span><span class="ss">:noreply</span><span class="p">,</span> <span class="n">cancel_async</span><span class="p">(</span><span class="n">socket</span><span class="p">,</span> <span class="ss">:data_stream</span><span class="p">)}</span>
  <span class="k">end</span>

  <span class="nv">@impl</span> <span class="no">true</span>
  <span class="k">def</span> <span class="n">handle_async</span><span class="p">(</span><span class="ss">:data_stream</span><span class="p">,</span> <span class="p">{</span><span class="ss">:exit</span><span class="p">,</span> <span class="p">{</span><span class="ss">:shutdown</span><span class="p">,</span> <span class="ss">:cancel</span><span class="p">}},</span> <span class="n">socket</span><span class="p">)</span> <span class="k">do</span>
    <span class="p">{</span><span class="ss">:noreply</span><span class="p">,</span> <span class="n">assign</span><span class="p">(</span><span class="n">socket</span><span class="p">,</span> <span class="ss">:async_state</span><span class="p">,</span> <span class="no">AsyncResult</span><span class="o">.</span><span class="n">ok</span><span class="p">(</span><span class="s2">"cancelled"</span><span class="p">))}</span>
  <span class="k">end</span>

  <span class="nv">@impl</span> <span class="no">true</span>
  <span class="k">def</span> <span class="n">handle_info</span><span class="p">({</span><span class="ss">:render_lorem</span><span class="p">,</span> <span class="n">word</span><span class="p">},</span> <span class="n">socket</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">lorem</span> <span class="o">=</span> <span class="n">socket</span><span class="o">.</span><span class="n">assigns</span><span class="o">.</span><span class="n">lorem</span> <span class="o">&lt;&gt;</span> <span class="s2">" "</span> <span class="o">&lt;&gt;</span> <span class="n">word</span>

    <span class="p">{</span><span class="ss">:noreply</span><span class="p">,</span> <span class="n">assign</span><span class="p">(</span><span class="n">socket</span><span class="p">,</span> <span class="ss">lorem:</span> <span class="n">lorem</span><span class="p">)}</span>
  <span class="k">end</span>

  <span class="k">defp</span> <span class="n">stream_response</span><span class="p">(</span><span class="n">pid</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">word</span> <span class="o">=</span>
      <span class="s2">"Lorem ipsum dolor sit amet, ullum phaedrum in est, sit viris dissentiunt eu. Ad qui aperiri senserit necessitatibus. In ferri persius vel, te option saperet pertinacia sit. At duis nulla zril per. Alienum accumsan qui ei, at quem constituto pri, ei facer libris cum. Doctus integre blandit pri an, quas intellegam quaerendum eu per."</span>
      <span class="o">|&gt;</span> <span class="no">String</span><span class="o">.</span><span class="n">split</span><span class="p">()</span>
      <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">random</span><span class="p">()</span>

    <span class="no">Process</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">150</span><span class="p">)</span>

    <span class="n">send</span><span class="p">(</span><span class="n">pid</span><span class="p">,</span> <span class="p">{</span><span class="ss">:render_lorem</span><span class="p">,</span> <span class="n">word</span><span class="p">})</span>
    <span class="n">stream_response</span><span class="p">(</span><span class="n">pid</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="go">Go!</h3>

<p>Clicking the “Go!” button starts everything off. The <code class="language-plaintext highlighter-rouge">handle_event/3</code> function
on line 39 first sets the <code class="language-plaintext highlighter-rouge">pid</code> variable to the current process. Next, it
assigns the <code class="language-plaintext highlighter-rouge">:async_state</code> to “loading”, and finally starts <code class="language-plaintext highlighter-rouge">stream_response/1</code>
function by using the <code class="language-plaintext highlighter-rouge">start_async/4</code> function.</p>

<p><code class="language-plaintext highlighter-rouge">stream_response/1</code> might look a little complicated, but all it does is grab a
random word from the Lorem Ipsum string, sleeps for 150 milliseconds, sends the
word to the parent process, and then call itself again (i.e. recursion). We pass
the <code class="language-plaintext highlighter-rouge">pid</code> to <code class="language-plaintext highlighter-rouge">stream_response/1</code>, because <code class="language-plaintext highlighter-rouge">start_async/4</code> starts it as a child
process and we use that child process to return the data to the parent. By
passing the parent PID, the child process can utilize message passing to update
the UI.</p>

<p>The final piece of the puzzle is <code class="language-plaintext highlighter-rouge">handle_info/2</code> on line 71. This function
matches messages sent with <code class="language-plaintext highlighter-rouge">{:render_lorem, word}</code>, (e.g. sent from
<code class="language-plaintext highlighter-rouge">stream_response/1</code>.) It then appends the word to the <code class="language-plaintext highlighter-rouge">:lorem</code> assigns variable.
At that point, the page is updated with the new string.</p>

<p><img src="https://samuelmullen.com/assets/images/asynchronous_tasks_and_streaming_uis/streaming_capture.gif" class="img-thumbnail" /></p>

<p>Most applications won’t need streaming data. Instead, you’ll use
<code class="language-plaintext highlighter-rouge">start_async/4</code>/<code class="language-plaintext highlighter-rouge">handle_async/3</code> to perform multiple, related tasks and assign
the results as needed. An example might be to fetch stock data from an external
source, store the results to the database, and then pull the list of stock
data from the database to recalculate and display on the page.</p>

<h3 id="cancel">Cancel</h3>

<p>When you click the “Cancel” button, it triggers the “cancel” event on line 61.
This event first sets the <code class="language-plaintext highlighter-rouge">:async_state</code> to “cancelled”, and then uses the
<code class="language-plaintext highlighter-rouge">cancel_async/3</code> function to terminate functions spawned by <code class="language-plaintext highlighter-rouge">start_async/4</code>.
This also has the side effect of executing the <code class="language-plaintext highlighter-rouge">handle_async/3</code> function with
the <code class="language-plaintext highlighter-rouge">{:exit, {:shutdown, :cancel}}</code> tuple, which just sets <code class="language-plaintext highlighter-rouge">:async_state</code> to
cancelled.</p>

<h3 id="reset">Reset</h3>

<p>The last feature is the “Reset” button. When clicked, this sends the “reset”
event to the <code class="language-plaintext highlighter-rouge">handle_event/3</code> function on line 52, which then sets
<code class="language-plaintext highlighter-rouge">:async_state</code> to “Reset” using <code class="language-plaintext highlighter-rouge">AsyncResult.ok/1</code>, and sets <code class="language-plaintext highlighter-rouge">:lorem</code> to
an empty string.</p>

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

<p>The evolution of Phoenix LiveView has turned what used to be a complex
orchestration of manual process management into a streamlined, declarative
developer experience. By embracing the “async” suite of functions, you can
adhere to the programmer’s virtue of laziness—writing less boilerplate while
achieving more robust results.</p>

<h3 id="choosing-the-right-tool">Choosing the Right Tool</h3>

<p>To help you decide which function fits your specific needs, here is a quick
reference:</p>

<table>
  <thead>
    <tr>
      <th>Function</th>
      <th>Best For…</th>
      <th>Key Advantage</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>assign_async/4</td>
      <td>Simple data fetching</td>
      <td>Minimal setup; handles the AsyncResult state automatically.</td>
    </tr>
    <tr>
      <td>stream_async/4</td>
      <td>Initial page loads of collection data.</td>
      <td>Integrates directly with Phoenix Streams for efficient DOM updates.</td>
    </tr>
    <tr>
      <td>start_async/4 &amp; handle_async/3</td>
      <td>Complex workflows, streaming, and cancellations.</td>
      <td>Full control over the process lifecycle and manual messaging.</td>
    </tr>
  </tbody>
</table>

<p>While Sean Moriarity’s original approach was an excellent solution when written,
the introduction of these native async utilities means we no longer have to
“fight” the framework to handle long-running tasks. Whether you are building
dashboards or a real-time LLM interface, these tools allow you to keep the
UI responsive and your code maintainable.</p>]]></content><author><name></name></author><category term="elixir" /><category term="liveview" /><category term="phoenix" /><summary type="html"><![CDATA[A guide to the native asynchronous suite in Phoenix LiveView. Master assign_async/4, stream_async/4, and start_async/4/handle_async/3 to make working with streaming and background tasks easy.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://samuelmullen.com/assets/images/asynchronous_tasks_and_streaming_uis/async_streaming.png" /><media:content medium="image" url="https://samuelmullen.com/assets/images/asynchronous_tasks_and_streaming_uis/async_streaming.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Differences Between Elixir’s Protocols and Behaviours</title><link href="https://samuelmullen.com/articles/differences-between-elixirs-protocols-and-behaviours" rel="alternate" type="text/html" title="Differences Between Elixir’s Protocols and Behaviours" /><published>2025-11-18T05:00:00+00:00</published><updated>2025-11-18T05:00:00+00:00</updated><id>https://samuelmullen.com/articles/differences-between-elixirs-protocols-and-behaviours</id><content type="html" xml:base="https://samuelmullen.com/articles/differences-between-elixirs-protocols-and-behaviours"><![CDATA[<p>I second guess myself a lot about what I know, or what I think I know. It’s even
worse when it comes to the names of people I don’t normally interact with: “I’m
almost positive his name is Dave, but I don’t want to get it wrong, so I’ll just
wait until someone else says his name.” It’s a solution, but I’d rather be
certain about their names.</p>

<p>In my previous article, <a href="https://samuelmullen.com/articles/crawling-the-web-with-elixirs-broadway-and-wallaby">Crawling the Web with Elixir’s Broadway and
Wallaby</a>,
I suggested that it might be “beneficial to use either a Protocol or Behaviour
to reduce code duplication,” but when considering which to use for what I’m
working on, it left me second-guessing myself yet again. What’s the difference?
When would you use one over the other? How do they actually help?</p>

<p>Because I don’t think I’m alone in my confusion, I’m going to try to answer
these questions by first explaining what I thought I understood about both,
talking about what I got wrong, showing the differences, and then giving a
real-world—if contrived and incomplete—example for when you might
use both.</p>

<h2 id="what-i-knew-about-protocols">What I Knew About Protocols</h2>

<p>I thought I had a pretty good handle on protocols; I even wrote <a href="https://samuelmullen.com/articles/elixir-protocols">an article
about them</a>. I knew they
provided, “a mechanism to achieve polymorphism in Elixir.” That is,</p>

<blockquote>
  <p>by implementing functions specific to a protocol, we make sure our structs and
built-in data types can take advantage of everything the implemented library
has to offer.</p>
</blockquote>

<p>I also knew they provided a way to extend a module’s functionality without
having access to the module’s source code.</p>

<h2 id="what-i-thought-i-knew-about-behaviours">What I Thought I Knew About Behaviours</h2>

<p>I’m embarrassed to admit it, but I thought behaviours were only used to define
what functions a module had to have. That’s not entirely wrong, but it misses
the “why?”. I came to this conclusion after reading José’s article, <a href="https://dashbit.co/blog/mocks-and-explicit-contracts">Mocks and
Explicit Contracts</a>. In
that article he shows how to create mocks by switching out one module for
another in a test environment, but it only worked if both modules, the original
and the mock, conformed to the same contract, i.e. Behaviour. Because the
Behaviour example from the article did nothing other than define callbacks, I
concluded that that’s all they were used for.</p>

<p>This conclusion was further solidified after hearing an Elixir podcast host
state that he thought Behaviours were best used in libraries rather than in your
project, because you have control over the modules in your own project. That’s
probably not what he meant, but it’s what I heard and it conformed to what I was
already thinking: Behaviours are kind of useless.</p>

<h2 id="what-i-got-wrong">What I Got Wrong</h2>

<p>There were three things I got wrong about the Protocols and Behaviours.</p>

<h3 id="protocols-are-just-specialized-behaviours">Protocols are Just Specialized Behaviours</h3>

<p>The first misunderstanding I had was thinking Protocols were Behaviours with
added functionality. I came to that conclusion from the following quote:</p>

<blockquote>
  <p>Protocol is a behaviour with the dispatching logic so you don’t need to hand
roll it nor impose a particular implementation in the user module.</p>

  <p>– José Valim - <a href="https://groups.google.com/g/elixir-lang-talk/c/S0NlOoc4ThM/m/J2aD2hKrtuoJ?pli=1">Google Group discussion</a></p>
</blockquote>

<p>It’s true that “[a] protocol is indeed a behaviour + dispatching logic,” but
what I misunderstood was thinking a Protocol was interchangeable with a
Behaviour. They are not.</p>

<h3 id="behaviours-dont-provide-functionality">Behaviours Don’t Provide Functionality</h3>

<p>Maybe it’s because I knew about behaviors in C# or because I had heard
“contract” used in relation to Behaviours, but for whatever reason, I had it in
my mind that Behaviours only <em>defined</em> a module’s functionality, but didn’t
<em>provide</em> any itself. I can’t defend this misunderstanding. If I had taken five
minutes to think about how <code class="language-plaintext highlighter-rouge">GenServer</code> or <code class="language-plaintext highlighter-rouge">Plug</code> worked, I would have quickly
abandoned that idea: both Behaviours obviously provide lots of functionality.</p>

<h3 id="behaviours-were-better-suited-in-libraries">Behaviours Were Better Suited in Libraries</h3>

<p>The idea that Behaviours are better suited in libraries than in your projects
came from a couple places. I first heard the argument on a podcast, (which shall
remain nameless) but I don’t think it’s what the speaker meant. And because I
already had the idea that Protocols were just specialized Behaviours, and
Behaviours didn’t provide functionality, it was an easy, if erroneous,
conclusion to reach.</p>

<h2 id="what-are-the-differences">What are the Differences</h2>

<p>Protocols and Behaviours differ primarily by what they execute against.
Protocols work with data types, while Behaviours execute against modules. Every
other difference is based off these fundamental concepts.</p>

<blockquote>
  <p>…a behaviour is internal to a module–the module implements the behaviour.
Protocols are different–you can place a protocol’s implementation completely
outside the module. This means you can extend modules functionality without
having to add code to them…”</p>

  <p>– Dave Thomas, <em>Programming Elixir</em></p>
</blockquote>

<h3 id="protocols-dont-provide-functionality">Protocols Don’t Provide Functionality</h3>

<p>The first thing to notice when working with Protocols—that is, if you are
creating one—is that they don’t provide functionality, they define it.
Consider the <code class="language-plaintext highlighter-rouge">Enumerable</code> Protocol:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defprotocol</span> <span class="no">Enumerable</span> <span class="k">do</span>
  <span class="c1"># documentation, type definitions, and specs have been removed</span>
  <span class="k">def</span> <span class="n">reduce</span><span class="p">(</span><span class="n">enumerable</span><span class="p">,</span> <span class="n">acc</span><span class="p">,</span> <span class="n">fun</span><span class="p">)</span>

  <span class="k">def</span> <span class="n">count</span><span class="p">(</span><span class="n">enumerable</span><span class="p">)</span>

  <span class="k">def</span> <span class="n">member?</span><span class="p">(</span><span class="n">enumerable</span><span class="p">,</span> <span class="n">element</span><span class="p">)</span>

  <span class="k">def</span> <span class="n">slice</span><span class="p">(</span><span class="n">enumerable</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>It’s not until you <em>implement</em> the Protocol, that you gain functionality.</p>

<p>Example:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">User</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:id</span><span class="p">,</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">:age</span><span class="p">]</span>

  <span class="k">defimpl</span> <span class="no">Inspect</span> <span class="k">do</span>
    <span class="k">def</span> <span class="n">inspect</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">_opts_</span><span class="p">)</span> <span class="k">do</span>
      <span class="s2">"</span><span class="si">#{</span><span class="n">user</span><span class="o">.</span><span class="n">name</span><span class="si">}</span><span class="s2"> (</span><span class="si">#{</span><span class="n">user</span><span class="o">.</span><span class="n">age</span><span class="si">}</span><span class="s2">)"</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="c1"># iEX</span>

<span class="n">iex</span> <span class="p">::</span> <span class="mi">1</span> <span class="o">&gt;</span> <span class="no">IO</span><span class="o">.</span><span class="n">inspect</span> <span class="p">%</span><span class="no">User</span><span class="p">{</span><span class="ss">id:</span> <span class="s1">'10-289", name: "John Galt", age: 38}
John Galt (38)
</span></code></pre></div></div>

<h3 id="protocols-work-on-data-types">Protocols Work on Data Types</h3>

<p>Unlike Behaviours which work at the Module level, Protocols work on data types.
In the previous example, where we implemented the <code class="language-plaintext highlighter-rouge">Inspect</code> protocol on <code class="language-plaintext highlighter-rouge">User</code>,
a <code class="language-plaintext highlighter-rouge">%User{}</code> struct is passed to <code class="language-plaintext highlighter-rouge">IO.inspect/2</code>. The <code class="language-plaintext highlighter-rouge">inspect/2</code> function is then
able to do something with it because <code class="language-plaintext highlighter-rouge">User</code> defines how <code class="language-plaintext highlighter-rouge">Inspect</code> should handle
it.</p>

<blockquote>
  <p>Protocol is type/data based polymorphism. When I call Enum.each(foo, …), the
concrete enumeration is determined from the type of foo.</p>

  <p>– Sasa Juric <a href="https://stackoverflow.com/questions/26215206/difference-between-protocol-behaviour-in-elixir">StackOverflow answer</a></p>
</blockquote>

<p>At runtime, Protocols allow us to execute the appropriate logic against the
specific datatype. This is dynamic dispatching.</p>

<h3 id="protocols-allow-polymorphism">Protocols Allow Polymorphism</h3>

<p>In OOP, polymorphism allows objects of different classes to be treated as
instances of a common superclass. You get a similar behavior in Elixir when you
implement a Protocol in your datatypes.</p>

<blockquote>
  <p><em>Polymorphism</em> is a runtime decision about which code to execute, based on
the nature of the input data. In Elixir, the basic (but not the only) way of
doing this is by using the language feature called <em>protocols</em>.</p>

  <p>– Sasa Juric</p>
</blockquote>

<p>As we saw when we implemented the <code class="language-plaintext highlighter-rouge">Inspect</code> Protocol in the <code class="language-plaintext highlighter-rouge">%User{}</code> struct
above, any datatype that implements the <code class="language-plaintext highlighter-rouge">Inspect</code> protocol can be passed to
functions like <code class="language-plaintext highlighter-rouge">Kernel.inspect/2</code> and <code class="language-plaintext highlighter-rouge">IO.inspect/2</code> and Elixir figures out how
to handle each at runtime based on the datatype implementation.</p>

<h3 id="protocols-extend-module-behavior">Protocols Extend Module Behavior</h3>

<p>If you’ve come to Elixir from another language like Ruby or JavaScript, you
might be familiar with the term, “monkey patching.” Monkey patching allows us
to “open up” a class or object and add or overwrite functionality. While you
can’t overwrite functions in Elixir, you can extend functionality through the
use of Protocols.</p>

<blockquote>
  <p>protocols allow us to extend the original behavior for as many data types as
we need. That’s because dispatching on a protocol is available to any data
type that has implemented the protocol and a protocol can be implemented by
anyone, at any time.</p>

  <p>– Elixir Lang</p>
</blockquote>

<p>As an example, if you had the following <code class="language-plaintext highlighter-rouge">Emptiness</code> protocol in your codebase…</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defprotocol</span> <span class="no">Emptiness</span> <span class="k">do</span>
  <span class="nv">@doc</span> <span class="s2">"Returns a boolean value based on the 'emptiness' of the term"</span>
  <span class="nv">@spec</span> <span class="n">empty?</span><span class="p">(</span><span class="n">term</span><span class="p">)</span> <span class="p">::</span> <span class="n">boolean</span><span class="p">()</span>
  <span class="k">def</span> <span class="n">empty?</span><span class="p">(</span><span class="n">t</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>…you could extend any Elixir type with <code class="language-plaintext highlighter-rouge">Emptiness</code> regardless of whether or
not it was a 1st-party type or if it was included as a library.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defimpl</span> <span class="no">Emptiness</span><span class="p">,</span> <span class="ss">for:</span> <span class="no">Plug</span><span class="o">.</span><span class="no">Conn</span> <span class="k">do</span>
  <span class="k">def</span> <span class="n">empty?</span><span class="p">(%</span><span class="no">Plug</span><span class="o">.</span><span class="no">Conn</span><span class="p">{</span><span class="ss">resp_body:</span> <span class="no">nil</span><span class="p">}),</span> <span class="k">do</span><span class="p">:</span> <span class="no">true</span>
  <span class="k">def</span> <span class="n">empty?</span><span class="p">(%</span><span class="no">Plug</span><span class="o">.</span><span class="no">Conn</span><span class="p">{</span><span class="ss">resp_body:</span> <span class="n">body</span><span class="p">}),</span> <span class="k">do</span><span class="p">:</span> <span class="n">length</span><span class="p">(</span><span class="n">body</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span>
<span class="k">end</span>
</code></pre></div></div>

<p>In this example, even though we didn’t create <code class="language-plaintext highlighter-rouge">Plug.Conn</code>, we can still add new
functionality with the <code class="language-plaintext highlighter-rouge">Emptiness</code> Protocol.</p>

<h3 id="behaviours-require-conformity">Behaviours Require Conformity</h3>

<p>Protocols don’t require you to implement every function definition, but if you
don’t you may not get every available feature. For example, if you only
implement the <code class="language-plaintext highlighter-rouge">Enumerable.reduce/2</code> function in your module, you’ll only be able
to pass your module type to some of <code class="language-plaintext highlighter-rouge">Enum</code>’s functions. With Behaviours, on the
other hand, you either implement every associated function or your application
doesn’t run.</p>

<blockquote>
  <p>A module that declares that it implements a particular behaviour must
implement all of the associated functions. If it doesn’t, Elixir will generate
a compilation warning.</p>

  <p>– Dave Thomas, <em>Programming Elixir</em></p>
</blockquote>

<p>And again…</p>

<blockquote>
  <p>By declaring that our module implements that behaviour, we let the compiler
validate that we have actually supplied the necessary interface. This reduces
the chance of an unexpected runtime error.”</p>

  <p>– Dave Thomas, <em>Programming Elixir</em></p>
</blockquote>

<h3 id="behaviours-execute-modules">Behaviours Execute Modules</h3>

<p>As stated under <a href="what-are-the-differences">What are the Differences</a>, “Protocols
and Behaviours differ primarily by what they are executed against. Protocols
work with data types, while Behaviours execute against modules.” In order for
this to work, modules must conform to the Behaviour by implementing the required
functions.</p>

<blockquote>
  <p>Behaviour is a typeless plug-in mechanism. When I call
<code class="language-plaintext highlighter-rouge">GenServer.start(MyModule)</code>, I explicitly pass MyModule as a plug-in, and the
generic code from GenServer will call into this module when needed.</p>

  <p>– Sasa Juric <a href="https://stackoverflow.com/questions/26215206/difference-between-protocol-behaviour-in-elixir">StackOverflow answer</a></p>
</blockquote>

<h3 id="behaviours-define-and-implement-common-logic">Behaviours Define and Implement Common Logic</h3>

<p>Behaviours provide a way of abstracting away functionality that is common across
every module that would implement them. For example, when you create a module
using the <code class="language-plaintext highlighter-rouge">GenServer</code> Behaviour and implement <code class="language-plaintext highlighter-rouge">init/1</code> and <code class="language-plaintext highlighter-rouge">handle_cast/2</code>, you
don’t think about the loop it runs in, or handling state. The <code class="language-plaintext highlighter-rouge">GenServer</code>
Behaviour provides that functionality. All you need to worry about is
implementing the required functions.</p>

<blockquote>
  <p>A behaviour is a way to say: give me a module as argument and I will invoke
the following callbacks on it, which these argument and so on. A more complex
example for behaviours besides a GenServer are the Ecto adapters.</p>

  <p>– José Valim <a href="https://groups.google.com/g/elixir-lang-talk/c/S0NlOoc4ThM/m/J2aD2hKrtuoJ?pli=1">Google Group discussion</a></p>
</blockquote>

<p>Usually the provided functionality is accomplished through meta programming, and
you would <code class="language-plaintext highlighter-rouge">use</code> the specific Behaviour, but it’s not required and there’s no
reason you couldn’t populate the Behaviour module with “normal” functions for
added utility.</p>

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

<p>We get things wrong all the time. We miss meetings, include or exclude the wrong
features, work on the wrong tasks, and the list goes on. Sometimes these
mistakes are due to a misunderstanding, sometimes it’s poor communication, and
sometimes it’s failing to check your premises. With regard to Protocols and
Behaviours, for me it was a little bit of everything.</p>

<p>What I discovered, however, is that Behaviours and Protocols, while
superficially similar, serve very different roles in the Elixir ecosystem.
Protocols provide extensibility and polymorphism to your types, while Behaviours
provide functionality to and demand conformity from your modules. Behaviours are
about plugging in modules (<code class="language-plaintext highlighter-rouge">GenServer</code>, <code class="language-plaintext highlighter-rouge">Plug</code>, <code class="language-plaintext highlighter-rouge">Ecto.Repo</code>, etc.). Protocols
are about plugging in data types (<code class="language-plaintext highlighter-rouge">Enumerable</code>, <code class="language-plaintext highlighter-rouge">Jason.Encoder</code>, <code class="language-plaintext highlighter-rouge">String.Chars</code>,
etc.). Once you see it that way, you’ll never second-guess yourself again.</p>

<h2 id="example">Example</h2>

<p>What follows is a completely contrived and incomplete example of how one might
use Behaviours and Protocols together. The idea is that you might want to update
a local User’s identity with information from a social network, and also update
the social network’s information from that changed locally.</p>

<p>Here is both an example Behaviour and Protocol:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># SocialProfile Behaviour</span>

<span class="k">defmodule</span> <span class="no">SocialProfile</span> <span class="k">do</span>
  <span class="nv">@callback</span> <span class="n">get_profile</span><span class="p">(</span><span class="no">Identity</span><span class="o">.</span><span class="n">t</span><span class="p">)</span> <span class="p">::</span> <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="n">term</span><span class="p">}</span> <span class="o">|</span> <span class="p">{</span><span class="ss">:error</span><span class="p">,</span> <span class="n">atom</span><span class="p">}</span>

  <span class="nv">@callback</span> <span class="n">update_profile</span><span class="p">(</span><span class="no">Profile</span><span class="o">.</span><span class="n">t</span><span class="p">)</span> <span class="p">::</span> <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="n">term</span><span class="p">}</span> <span class="o">|</span> <span class="p">{</span><span class="ss">:error</span><span class="p">,</span> <span class="n">atom</span><span class="p">}</span>

  <span class="nv">@callback</span> <span class="n">to_profile</span><span class="p">(</span><span class="no">Identity</span><span class="o">.</span><span class="n">t</span><span class="p">)</span> <span class="p">::</span> <span class="n">term</span> <span class="o">|</span> <span class="p">{</span><span class="ss">:error</span><span class="p">,</span> <span class="n">atom</span><span class="p">}</span>
<span class="k">end</span>

<span class="c1"># Identifier Protocol</span>

<span class="k">defprotocol</span> <span class="no">Identifier</span> <span class="k">do</span>
  <span class="k">def</span> <span class="n">to_identity</span><span class="p">(</span><span class="n">profile</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Modules implementing the <code class="language-plaintext highlighter-rouge">SocialProfile</code> Behaviour are required to implement
the functions: <code class="language-plaintext highlighter-rouge">get_profile/1</code>, <code class="language-plaintext highlighter-rouge">update_profile/1</code>, <code class="language-plaintext highlighter-rouge">to_profile/1</code>, and
follow the TypeSpecs provided.</p>

<p>The <code class="language-plaintext highlighter-rouge">Identifier</code> Protocol requires that any module implementing it create the
<code class="language-plaintext highlighter-rouge">to_identity/1</code> function. You would use this to transform social media profiles
into local Identities.</p>

<p>Here are two Profiles you might create:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># x_profile.ex</span>

<span class="k">defmodule</span> <span class="no">XProfile</span> <span class="k">do</span>
  <span class="nv">@behaviour</span> <span class="no">SocialProfile</span>

  <span class="nv">@impl</span> <span class="no">SocialProfile</span>
  <span class="k">def</span> <span class="n">get_profile</span><span class="p">(</span><span class="n">identity</span><span class="p">)</span> <span class="k">do</span>
    <span class="c1"># logic to retrieve profile based on provided identity</span>
  <span class="k">end</span>

  <span class="nv">@impl</span> <span class="no">SocialProfile</span>
  <span class="k">def</span> <span class="n">update_profile</span><span class="p">(</span><span class="n">profile</span><span class="p">)</span> <span class="k">do</span>
    <span class="c1"># logic to update profile</span>
  <span class="k">end</span>

  <span class="nv">@impl</span> <span class="no">SocialProfile</span>
  <span class="k">def</span> <span class="n">to_profile</span><span class="p">(</span><span class="n">identity</span><span class="p">)</span> <span class="k">do</span>
    <span class="p">[</span><span class="n">first_name</span><span class="p">,</span> <span class="n">last_name</span><span class="p">]</span> <span class="o">=</span> <span class="no">String</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="n">identity</span><span class="o">.</span><span class="n">name</span><span class="p">)</span>
    <span class="p">%{</span>
      <span class="ss">first_name:</span> <span class="n">first_name</span><span class="p">,</span>
      <span class="ss">last_name:</span> <span class="n">last_name</span><span class="p">,</span>
      <span class="ss">email:</span> <span class="n">identity</span><span class="o">.</span><span class="n">email</span><span class="p">,</span>
      <span class="ss">bio:</span> <span class="n">identity</span><span class="o">.</span><span class="n">bio</span>
    <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="c1"># Protocol implemented outside of module</span>
<span class="k">defimpl</span> <span class="no">Identifier</span><span class="p">,</span> <span class="ss">for:</span> <span class="no">XProfile</span> <span class="k">do</span>
  <span class="k">def</span> <span class="n">to_identity</span><span class="p">(</span><span class="n">profile</span><span class="p">)</span> <span class="k">do</span>
    <span class="p">%</span><span class="no">Identity</span><span class="p">{</span>
      <span class="ss">name:</span> <span class="s2">"</span><span class="si">#{</span><span class="n">profile</span><span class="o">.</span><span class="n">first_name</span><span class="si">}</span><span class="s2"> </span><span class="si">#{</span><span class="n">profile</span><span class="o">.</span><span class="n">last_name</span><span class="si">}</span><span class="s2">"</span><span class="p">,</span>
      <span class="ss">email:</span> <span class="n">profile</span><span class="o">.</span><span class="n">email</span><span class="p">,</span>
      <span class="ss">bio:</span> <span class="n">profile</span><span class="o">.</span><span class="n">bio</span>
    <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="c1"># github_profile.ex</span>

<span class="k">defmodule</span> <span class="no">GitHubProfile</span> <span class="k">do</span>
  <span class="nv">@behaviour</span> <span class="no">SocialProfile</span>

  <span class="nv">@impl</span> <span class="no">SocialProfile</span>
  <span class="k">def</span> <span class="n">get_profile</span><span class="p">(</span><span class="n">identity</span><span class="p">)</span> <span class="k">do</span>
    <span class="c1"># logic to retrieve profile based on provided identity</span>
  <span class="k">end</span>

  <span class="nv">@impl</span> <span class="no">SocialProfile</span>
  <span class="k">def</span> <span class="n">update_profile</span><span class="p">(</span><span class="n">profile</span><span class="p">)</span> <span class="k">do</span>
    <span class="c1"># logic to update profile</span>
  <span class="k">end</span>

  <span class="nv">@impl</span> <span class="no">SocialProfile</span>
  <span class="k">def</span> <span class="n">to_profile</span><span class="p">(</span><span class="n">identity</span><span class="p">)</span> <span class="k">do</span>
    <span class="p">%{</span>
      <span class="ss">name:</span> <span class="n">identity</span><span class="o">.</span><span class="n">name</span><span class="p">,</span>
      <span class="ss">email:</span> <span class="n">identity</span><span class="o">.</span><span class="n">email</span><span class="p">,</span>
      <span class="ss">description:</span> <span class="n">identity</span><span class="o">.</span><span class="n">bio</span>
    <span class="p">}</span>
  <span class="k">end</span>

  <span class="c1"># Protocol implemented inside module</span>
  <span class="k">defimpl</span> <span class="no">Identifier</span> <span class="k">do</span>
    <span class="k">def</span> <span class="n">to_identity</span><span class="p">(</span><span class="n">profile</span><span class="p">)</span> <span class="k">do</span>
      <span class="p">%</span><span class="no">Identity</span><span class="p">{</span>
        <span class="ss">name:</span> <span class="n">profile</span><span class="o">.</span><span class="n">name</span><span class="p">,</span>
        <span class="ss">email:</span> <span class="n">profile</span><span class="o">.</span><span class="n">email</span><span class="p">,</span>
        <span class="ss">bio:</span> <span class="n">profile</span><span class="o">.</span><span class="n">description</span>
      <span class="p">}</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The main difference between the two is the use of <code class="language-plaintext highlighter-rouge">first_name</code> and <code class="language-plaintext highlighter-rouge">last_name</code>
on the <code class="language-plaintext highlighter-rouge">XProfile</code> versus just having a <code class="language-plaintext highlighter-rouge">name</code> in GitHub. Because of that, we
need a little extra logic in <code class="language-plaintext highlighter-rouge">to_identity/1</code> and <code class="language-plaintext highlighter-rouge">to_profile/1</code> in <code class="language-plaintext highlighter-rouge">XProfile</code>.</p>

<p>I didn’t add example logic to either <code class="language-plaintext highlighter-rouge">get_profile/1</code> or <code class="language-plaintext highlighter-rouge">update_profile/1</code>
because it seemed like unnecessary effort. The purpose of both is clear.</p>

<p>Lastly, we have an example of how the above might be used:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Example Behaviour usage</span>

<span class="n">identity</span> <span class="o">=</span> <span class="no">Users</span><span class="o">.</span><span class="n">get_identity_by_email</span><span class="p">(</span><span class="s2">"john@galtsgulch.co"</span><span class="p">)</span>

<span class="p">[</span><span class="no">GitHubProfile</span><span class="p">,</span> <span class="no">XProfile</span><span class="p">]</span>
<span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">each</span><span class="p">(</span><span class="k">fn</span> <span class="n">social_profile</span> <span class="o">-&gt;</span>
  <span class="n">identity</span>
  <span class="o">|&gt;</span> <span class="n">social_profile</span><span class="o">.</span><span class="n">to_profile</span><span class="p">()</span>
  <span class="o">|&gt;</span> <span class="n">social_profile</span><span class="o">.</span><span class="n">update_profile</span><span class="p">()</span>
<span class="k">end</span><span class="p">)</span>
</code></pre></div></div>

<p>In this usage example, we are retrieving the User’s identity based on their
email. With that in hand, we are then able to update all of their social
profiles after first transforming the identity into the required profile.</p>

<p>In the next example, we retrieve the User’s profile from GitHub, and then
transform it into an Identity in order to update our local copy.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Example Protocol usage</span>

<span class="n">identity</span> <span class="o">=</span> <span class="no">Users</span><span class="o">.</span><span class="n">get_identity_by_email</span><span class="p">(</span><span class="s2">"john@galtsgulch.co"</span><span class="p">)</span>

<span class="n">profile</span> <span class="o">=</span> <span class="no">GitHubProfile</span><span class="o">.</span><span class="n">get_profile</span><span class="p">(</span><span class="n">identity</span><span class="p">)</span>

<span class="n">identity</span> <span class="o">=</span> <span class="no">Identifier</span><span class="o">.</span><span class="n">to_identity</span><span class="p">(</span><span class="n">profile</span><span class="p">)</span>

<span class="no">Users</span><span class="o">.</span><span class="n">update_identity</span><span class="p">(</span><span class="n">identity</span><span class="p">)</span>
</code></pre></div></div>]]></content><author><name></name></author><category term="elixir" /><summary type="html"><![CDATA[I kept mixing up Elixir Protocols and Behaviours—until I finally figured out the real difference. One is for data types, the other for modules. Here’s the explanation I wish I’d had years ago.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://samuelmullen.com/assets/images/protocols_v_behaviours/protocols_v_behaviours.png" /><media:content medium="image" url="https://samuelmullen.com/assets/images/protocols_v_behaviours/protocols_v_behaviours.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Crawling the Web with Elixir’s Broadway and Wallaby</title><link href="https://samuelmullen.com/articles/crawling-the-web-with-elixirs-broadway-and-wallaby" rel="alternate" type="text/html" title="Crawling the Web with Elixir’s Broadway and Wallaby" /><published>2025-11-06T05:00:00+00:00</published><updated>2025-11-06T05:00:00+00:00</updated><id>https://samuelmullen.com/articles/crawling-the-web-with-elixirs-broadway-and-wallaby</id><content type="html" xml:base="https://samuelmullen.com/articles/crawling-the-web-with-elixirs-broadway-and-wallaby"><![CDATA[<p>So it’s come to this: you need to programmatically retrieve data from a website
(or websites), but the usual tools aren’t cutting it. Maybe the
site uses a CDN and blocks tools like <a href="https://github.com/curl/curl">cURL</a> and
<a href="https://www.gnu.org/software/wget/">wget</a>. Or maybe it’s JavaScript-heavy and
what you normally use can’t execute the JS like a browser would, or maybe you
need something with more intelligence. Whatever the case, you’re at the
point where you need to build your own crawler.</p>

<p>Thankfully, Elixir’s <a href="https://github.com/dashbitco/broadway">Broadway</a> and
<a href="https://github.com/elixir-wallaby/wallaby">Wallaby</a> libraries provide an
excellent solution: Broadway to manage concurrency and load, and
Wallaby—while normally used for User Acceptance Testing—to handle
visiting and processing webpages; even JS-heavy sites.</p>

<p>In this article, we’ll quickly review where we’ve been with custom Broadway
producers, look at the architecture of where we want to go, and then go over the
additions and changes we need to make to get to that final solution.</p>

<h2 id="broadway-and-custom-producers">Broadway and Custom Producers</h2>

<aside class="panel panel-default pull-right col-md-4">
  This article builds on my previous articles, <a href="https://samuelmullen.com/articles/understanding-elixirs-broadway">Understanding Elixir's Broadway</a> and
  <a href="https://samuelmullen.com/articles/building-custom-producers-with-elixirs-broadway">Building Custom Producers with Elixir's Broadway</a>.
  If you haven't read those yet, you should stop reading this article and go
  read them first. This article is only going to confuse you without the
  foundation of the previous two.
</aside>

<p>As you probably know, Broadway is a sort of mini-framework built on top of the
<a href="https://github.com/elixir-lang/gen_stage">GenStage</a> library. It’s concurrent
and robust, and has built-in batching, rate limiting, and back-pressure. This
means that instead of accepting data “pushed” to it, it “pulls” the data from
its data sources via its producers. These data sources can be anything:
databases, Kafka streams, SQS queues, or, as we’ll see in this article,
websites.</p>

<p>To be able to pull data from websites, we’ll need a custom producer that’s able
to provide the Broadway consumer with URLs from which to fetch data. We’ve
already seen how to do that in <a href="https://samuelmullen.com/articles/building-custom-producers-with-elixirs-broadway">my previous article</a>,
and in this article we’ll tweak it slightly to customize how each site is
crawled.</p>

<p>Let’s look at what the architecture of our app will look like when completed.</p>

<h2 id="architecture">Architecture</h2>

<p>Here’s the basic architecture of the crawler. The <code class="language-plaintext highlighter-rouge">URLQueue</code> is where
everything starts. Once a URL is added to it, the <code class="language-plaintext highlighter-rouge">URLProducer</code> provides it to
the <code class="language-plaintext highlighter-rouge">Pipeline</code>, which tells the <code class="language-plaintext highlighter-rouge">Crawler</code>s to process it. The <code class="language-plaintext highlighter-rouge">Crawler</code>s then
find links on the page and adds them to the <code class="language-plaintext highlighter-rouge">URLQueue</code> to keep the cycle going.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>         +------------+
         |            |             +-------------+
         |  Pipeline  |&lt;------------| URLProducer |
         |            |             +-------------+
         +------------+                    ^
                |                          |
                v                          |
 + - - - - - - - - - - - - - -+            |
   Processors                              |
 |                            |      +----------+
    +---------+  +---------+   -----&gt;| URLQueue |
 |  | Crawler |  | Crawler |  |      +----------+
    +---------+  +---------+
 + - - - - - - - - - - - - - -+
        |           |
        v           |
 +-------------+    |        *----*----*
 | URLRegistry |    --------&gt;|  Magic  |
 +-------------+             *----*----*
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">Pipeline</code> is the Broadway consumer. This module defines what producers to
use, how many processors to run, how to transform the data provided by the
producer, and finally how to process it.</p>

<p>The <code class="language-plaintext highlighter-rouge">URLProducer</code> is a <code class="language-plaintext highlighter-rouge">GenStage</code> producer that pulls data from the <code class="language-plaintext highlighter-rouge">URLQueue</code>
and provides data to the <code class="language-plaintext highlighter-rouge">Pipeline</code> consumer. It remains unchanged from the
previous article.</p>

<p>The <code class="language-plaintext highlighter-rouge">URLQueue</code> is a <code class="language-plaintext highlighter-rouge">GenServer</code> used to keep track of what URLs to crawl next.
As mentioned previously, this is just a <code class="language-plaintext highlighter-rouge">List</code>, but could easily be a <code class="language-plaintext highlighter-rouge">:queue</code>
or even outsourced to SQS, Kafka, or some other datastore.</p>

<p>Crawlers are the workhorses of this architecture. The way we’re building it,
each site we want to crawl will have its own <code class="language-plaintext highlighter-rouge">Crawler</code>, which is responsible for
finding and storing links in the <code class="language-plaintext highlighter-rouge">URLQueue</code>, processing the webpage (i.e.
Magic), and registering the URL in the <code class="language-plaintext highlighter-rouge">URLRegistry</code> so it’s not crawled again.</p>

<p>Finally we have the <code class="language-plaintext highlighter-rouge">URLRegistry</code>. This <code class="language-plaintext highlighter-rouge">GenServer</code> is responsible for keeping a
list of URLs which have been crawled. If a URL is in the list, it should be
skipped by the <code class="language-plaintext highlighter-rouge">Crawler</code>. In this version, we’re using a <code class="language-plaintext highlighter-rouge">MapSet</code>, but for
serious work you should use a datastore like <code class="language-plaintext highlighter-rouge">ETS</code> or Redis.</p>

<h2 id="pipeline-v2">Pipeline v2</h2>

<p><code class="language-plaintext highlighter-rouge">Pipeline</code> is the Broadway consumer which provides configuration for the
producer and processors. For the sake of simplicity, we’re forgoing the use of
batching processes.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">Pipeline</span> <span class="k">do</span>
  <span class="kn">use</span> <span class="no">Broadway</span>

  <span class="n">alias</span> <span class="no">Broadway</span><span class="o">.</span><span class="no">Message</span>

  <span class="k">def</span> <span class="n">start_link</span><span class="p">(</span><span class="n">_opts</span><span class="p">)</span> <span class="k">do</span>
    <span class="no">Broadway</span><span class="o">.</span><span class="n">start_link</span><span class="p">(</span><span class="bp">__MODULE__</span><span class="p">,</span>
      <span class="ss">name:</span> <span class="bp">__MODULE__</span><span class="p">,</span>
      <span class="ss">producer:</span> <span class="p">[</span>
        <span class="ss">module:</span> <span class="p">{</span><span class="no">MyApp</span><span class="o">.</span><span class="no">URLProducer</span><span class="p">,</span> <span class="p">[]},</span>
        <span class="ss">transformer:</span> <span class="p">{</span><span class="bp">__MODULE__</span><span class="p">,</span> <span class="ss">:transform</span><span class="p">,</span> <span class="p">[]},</span>
        <span class="ss">concurrency:</span> <span class="mi">1</span>
      <span class="p">],</span>
      <span class="ss">processors:</span> <span class="p">[</span>
        <span class="ss">default:</span> <span class="p">[</span>
          <span class="ss">concurrency:</span> <span class="mi">2</span><span class="p">,</span>
          <span class="ss">min_demand:</span> <span class="mi">1</span><span class="p">,</span>
          <span class="ss">max_demand:</span> <span class="mi">2</span>
        <span class="p">]</span>
      <span class="p">]</span>
    <span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">handle_message</span><span class="p">(</span><span class="ss">:default</span><span class="p">,</span> <span class="p">%</span><span class="no">Message</span><span class="p">{</span><span class="ss">data:</span> <span class="p">%{</span><span class="ss">module:</span> <span class="n">module</span><span class="p">,</span> <span class="ss">url:</span> <span class="n">url</span><span class="p">}}</span> <span class="o">=</span> <span class="n">message</span><span class="p">,</span> <span class="n">_context</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">module</span><span class="o">.</span><span class="n">crawl</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
    <span class="n">message</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">transform</span><span class="p">(</span><span class="n">url_item</span><span class="p">,</span> <span class="n">_opts</span><span class="p">)</span> <span class="k">do</span>
    <span class="p">%</span><span class="no">Broadway</span><span class="o">.</span><span class="no">Message</span><span class="p">{</span>
      <span class="ss">data:</span> <span class="n">url_item</span><span class="p">,</span>
      <span class="ss">acknowledger:</span> <span class="no">Broadway</span><span class="o">.</span><span class="no">NoopAcknowledger</span><span class="o">.</span><span class="n">init</span><span class="p">()</span>
    <span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">ack</span><span class="p">(</span><span class="n">_ref</span><span class="p">,</span> <span class="n">_successes</span><span class="p">,</span> <span class="n">_failures</span><span class="p">)</span> <span class="k">do</span>
    <span class="ss">:ok</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>With the exception of the <code class="language-plaintext highlighter-rouge">handle_message/3</code> function, the module remains
unchanged from my previous article. You’ll note the <code class="language-plaintext highlighter-rouge">data</code> field in <code class="language-plaintext highlighter-rouge">%Message{}</code>
now expects a <code class="language-plaintext highlighter-rouge">Map</code> with keys of <code class="language-plaintext highlighter-rouge">module</code> and <code class="language-plaintext highlighter-rouge">url</code> instead of the previous
URL string. Also note that we’re calling the <code class="language-plaintext highlighter-rouge">crawl/1</code> function of the <code class="language-plaintext highlighter-rouge">module</code>
that’s passed in. We’ll get to that next.</p>

<h2 id="crawler">Crawler</h2>

<p>Unless you’re building a crawler for a search engine, LLM, or internet archiving
service—and if you are, this is not the article you should be
reading—you’ll only need to crawl a handful of sites. If that’s the
case, you’ll likely also have specific requirements you’re looking for: links to
follow, data to retrieve, and pages to skip. With that in mind, we’ll build our
<code class="language-plaintext highlighter-rouge">Crawler</code> to limit itself to a single site, <a href="https://iana.org">Internet Assigned Numbers
Authority</a> by starting the crawl at <a href="https://example.com">https://example.com</a>.</p>

<p>Before we begin, you’ll need to install Elixir’s
<a href="https://github.com/elixir-wallaby/wallaby">Wallaby</a> library (as you would any
other library) and Google’s
<a href="https://developer.chrome.com/docs/chromedriver">chromedriver</a> (On a mac: <code class="language-plaintext highlighter-rouge">brew
install chromedriver</code>).</p>

<p><strong>Note:</strong> You may also need to install <code class="language-plaintext highlighter-rouge">google-chrome</code> as well (<code class="language-plaintext highlighter-rouge">brew install
google-chrome</code>).</p>

<p>Below is our crawler, <code class="language-plaintext highlighter-rouge">MyApp.Crawlers.Example</code>, in its entirety. It looks like a
lot, but there’s only two public API functions—<code class="language-plaintext highlighter-rouge">init/0</code> and
<code class="language-plaintext highlighter-rouge">crawl/1</code>—both of which are described in more detail below. The module
itself is responsible for starting and stopping the <code class="language-plaintext highlighter-rouge">Wallaby</code> session, visiting
pages, capturing links to follow, and processing pages. We’ll break it down into
smaller chunks below.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">Crawlers</span><span class="o">.</span><span class="no">Example</span> <span class="k">do</span>
  <span class="n">alias</span> <span class="no">MyApp</span><span class="o">.</span><span class="p">{</span><span class="no">URLQueue</span><span class="p">,</span> <span class="no">URLRegistry</span><span class="p">}</span>
  <span class="n">alias</span> <span class="no">Wallaby</span><span class="o">.</span><span class="p">{</span><span class="no">Browser</span><span class="p">,</span> <span class="no">Element</span><span class="p">,</span> <span class="no">Query</span><span class="p">}</span>

  <span class="k">def</span> <span class="n">init</span><span class="p">()</span> <span class="k">do</span>
    <span class="no">URLQueue</span><span class="o">.</span><span class="n">push</span><span class="p">(%{</span>
      <span class="ss">module:</span> <span class="bp">__MODULE__</span><span class="p">,</span>
      <span class="ss">url:</span> <span class="s2">"https://example.com"</span>
    <span class="p">})</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">crawl</span><span class="p">(</span><span class="n">url</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">session</span><span class="p">()</span>
    <span class="o">|&gt;</span> <span class="no">Browser</span><span class="o">.</span><span class="n">visit</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
    <span class="o">|&gt;</span> <span class="n">process_links</span><span class="p">()</span>
    <span class="o">|&gt;</span> <span class="n">process_page</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
    <span class="o">|&gt;</span> <span class="no">Wallaby</span><span class="o">.</span><span class="n">end_session</span><span class="p">()</span>
  <span class="k">end</span>

  <span class="k">defp</span> <span class="n">session</span><span class="p">()</span> <span class="k">do</span>
    <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="n">session</span><span class="p">}</span> <span class="o">=</span> <span class="no">Wallaby</span><span class="o">.</span><span class="n">start_session</span><span class="p">(</span>
      <span class="ss">capabilities:</span> <span class="p">%{</span>
        <span class="ss">chromeOptions:</span> <span class="p">%{</span>
          <span class="ss">args:</span> <span class="p">[</span>
                <span class="s2">"--headless"</span><span class="p">,</span>
                <span class="s2">"--no-sandbox"</span><span class="p">,</span>
                <span class="s2">"window-size=1280,800"</span><span class="p">,</span>
                <span class="s2">"--fullscreen"</span><span class="p">,</span>
                <span class="s2">"--disable-gpu"</span><span class="p">,</span>
                <span class="s2">"--disable-dev-shm-usage"</span>
          <span class="p">]</span>
        <span class="p">}</span>
      <span class="p">}</span>
    <span class="p">)</span>

    <span class="n">session</span>
  <span class="k">end</span>

  <span class="k">defp</span> <span class="n">process_links</span><span class="p">(</span><span class="n">session</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">css_selectors</span> <span class="o">=</span> <span class="s2">"header a, footer a"</span>

    <span class="n">session</span>
    <span class="o">|&gt;</span> <span class="no">Browser</span><span class="o">.</span><span class="n">all</span><span class="p">(</span><span class="no">Query</span><span class="o">.</span><span class="n">css</span><span class="p">(</span><span class="n">css_selectors</span><span class="p">,</span> <span class="ss">minimum:</span> <span class="mi">0</span><span class="p">))</span>
    <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">each</span><span class="p">(</span><span class="o">&amp;</span><span class="n">store_link</span><span class="o">/</span><span class="mi">1</span><span class="p">)</span>

    <span class="n">session</span>
  <span class="k">end</span>

  <span class="k">defp</span> <span class="n">process_page</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">url</span><span class="p">)</span> <span class="k">do</span>
    <span class="no">IO</span><span class="o">.</span><span class="n">inspect</span> <span class="s2">"Processing page: </span><span class="si">#{</span><span class="n">url</span><span class="si">}</span><span class="s2">"</span>
    <span class="n">session</span>
  <span class="k">end</span>

  <span class="k">defp</span> <span class="n">store_link</span><span class="p">(</span><span class="n">element</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">element</span>
    <span class="o">|&gt;</span> <span class="no">Element</span><span class="o">.</span><span class="n">attr</span><span class="p">(</span><span class="s2">"href"</span><span class="p">)</span>
    <span class="o">|&gt;</span> <span class="n">_store_link</span><span class="p">()</span>
  <span class="k">end</span>

  <span class="k">defp</span> <span class="n">_store_link</span><span class="p">(</span><span class="s2">"https://www.iana.org/"</span> <span class="o">&lt;&gt;</span> <span class="n">_rest</span> <span class="o">=</span> <span class="n">url</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">_store_good_link</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">defp</span> <span class="n">_store_link</span><span class="p">(</span><span class="s2">"https://iana.org/"</span> <span class="o">&lt;&gt;</span> <span class="n">_rest</span> <span class="o">=</span> <span class="n">url</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">_store_good_link</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">defp</span> <span class="n">_store_link</span><span class="p">(</span><span class="n">url</span><span class="p">)</span> <span class="k">do</span>
    <span class="no">IO</span><span class="o">.</span><span class="n">inspect</span> <span class="n">url</span><span class="p">,</span> <span class="ss">label:</span> <span class="s2">"Some other link"</span>
    <span class="ss">:ok</span>
  <span class="k">end</span>

  <span class="k">defp</span> <span class="n">_store_good_link</span><span class="p">(</span><span class="n">url</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">url_item</span> <span class="o">=</span> <span class="p">%{</span>
      <span class="ss">url:</span> <span class="n">url</span><span class="p">,</span>
      <span class="ss">module:</span> <span class="bp">__MODULE__</span>
    <span class="p">}</span>

    <span class="k">case</span> <span class="no">URLRegistry</span><span class="o">.</span><span class="n">registered?</span><span class="p">(</span><span class="n">url_item</span><span class="p">)</span> <span class="k">do</span>
      <span class="no">true</span> <span class="o">-&gt;</span>
        <span class="ss">:ok</span>
      <span class="no">false</span> <span class="o">-&gt;</span>
        <span class="no">IO</span><span class="o">.</span><span class="n">inspect</span> <span class="n">url_item</span>
        <span class="no">URLRegistry</span><span class="o">.</span><span class="n">register</span><span class="p">(</span><span class="n">url_item</span><span class="p">)</span>
        <span class="no">URLQueue</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="n">url_item</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p><strong>Fun Fact:</strong> setting <code class="language-plaintext highlighter-rouge">css_selectors</code> to <code class="language-plaintext highlighter-rouge">"a"</code>, and changing <code class="language-plaintext highlighter-rouge">_store_link/1</code> to
accept any URL, will crawl the entire internet, but not before you’ve consumed
all your computer’s resources. I don’t recommend it.</p>

<h3 id="init0"><code class="language-plaintext highlighter-rouge">init/0</code></h3>

<p>As its name implies, <code class="language-plaintext highlighter-rouge">init/0</code> is responsible for initializing the Crawler into
action. It does so by “pushing” a <code class="language-plaintext highlighter-rouge">Map</code> containing the <code class="language-plaintext highlighter-rouge">module</code> and starting
<code class="language-plaintext highlighter-rouge">url</code>, in this case <code class="language-plaintext highlighter-rouge">MyApp.Crawlers.Example</code> and “https://example.com”
respectively. With this <code class="language-plaintext highlighter-rouge">Map</code> in the <code class="language-plaintext highlighter-rouge">URLQueue</code>, the <code class="language-plaintext highlighter-rouge">URLProducer</code> can retrieve
it and pass it to the <code class="language-plaintext highlighter-rouge">Pipeline</code>.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="n">init</span><span class="p">()</span> <span class="k">do</span>
  <span class="no">URLQueue</span><span class="o">.</span><span class="n">push</span><span class="p">(%{</span>
    <span class="ss">module:</span> <span class="bp">__MODULE__</span><span class="p">,</span>
    <span class="ss">url:</span> <span class="s2">"https://example.com"</span>
  <span class="p">})</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="crawl1"><code class="language-plaintext highlighter-rouge">crawl/1</code></h3>

<p>The <code class="language-plaintext highlighter-rouge">crawl/1</code> function accepts a URL in the form of a string and creates a
pipeline for processing that URL. It’s a very readable function, but there are a
couple things to take note of: 1) it starts by creating a <code class="language-plaintext highlighter-rouge">Wallaby</code> session
token and passes that along through the whole pipeline; 2) it finishes by ending
the <code class="language-plaintext highlighter-rouge">Wallaby</code> session. This last piece is very important. If you don’t end the
<code class="language-plaintext highlighter-rouge">Wallaby</code> session, you will end up with lots of <code class="language-plaintext highlighter-rouge">chromedriver</code> instances running
on your computer.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="n">crawl</span><span class="p">(</span><span class="n">url</span><span class="p">)</span> <span class="k">do</span>
  <span class="n">session</span><span class="p">()</span>
  <span class="o">|&gt;</span> <span class="no">Browser</span><span class="o">.</span><span class="n">visit</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
  <span class="o">|&gt;</span> <span class="n">process_links</span><span class="p">()</span>
  <span class="o">|&gt;</span> <span class="n">process_page</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
  <span class="o">|&gt;</span> <span class="no">Wallaby</span><span class="o">.</span><span class="n">end_session</span><span class="p">()</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="session0"><code class="language-plaintext highlighter-rouge">session/0</code></h3>

<p>The pipeline described in <code class="language-plaintext highlighter-rouge">crawl/1</code> passes a <code class="language-plaintext highlighter-rouge">Wallaby</code> <code class="language-plaintext highlighter-rouge">session</code> token to each
function. The <code class="language-plaintext highlighter-rouge">session</code> is created using the <code class="language-plaintext highlighter-rouge">start_session/1</code> function. The
<code class="language-plaintext highlighter-rouge">chromeOptions</code> used are what I found to be the minimum required options to
work. A full list of options can be found on the <a href="https://peter.sh/experiments/chromium-command-line-switches/">Chromium Command Line Switches
page</a>.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defp</span> <span class="n">session</span><span class="p">()</span> <span class="k">do</span>
  <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="n">session</span><span class="p">}</span> <span class="o">=</span> <span class="no">Wallaby</span><span class="o">.</span><span class="n">start_session</span><span class="p">(</span>
    <span class="ss">capabilities:</span> <span class="p">%{</span>
      <span class="ss">chromeOptions:</span> <span class="p">%{</span>
        <span class="ss">args:</span> <span class="p">[</span>
              <span class="s2">"--headless"</span><span class="p">,</span>
              <span class="s2">"--no-sandbox"</span><span class="p">,</span>
              <span class="s2">"window-size=1280,800"</span><span class="p">,</span>
              <span class="s2">"--fullscreen"</span><span class="p">,</span>
              <span class="s2">"--disable-gpu"</span><span class="p">,</span>
              <span class="s2">"--disable-dev-shm-usage"</span>
        <span class="p">]</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">)</span>

  <span class="n">session</span>
<span class="k">end</span>
</code></pre></div></div>

<p>I should also add that <code class="language-plaintext highlighter-rouge">Wallaby</code> itself can be configured from a <code class="language-plaintext highlighter-rouge">Config</code> file.
A basic example is provided below:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># my_app/config/config.exs</span>

<span class="kn">import</span> <span class="no">Config</span>

<span class="n">config</span> <span class="ss">:wallaby</span><span class="p">,</span>
  <span class="ss">js_errors:</span> <span class="no">false</span><span class="p">,</span>
  <span class="ss">driver:</span> <span class="no">Wallaby</span><span class="o">.</span><span class="no">Chrome</span><span class="p">,</span>
  <span class="ss">hackney_options:</span> <span class="p">[</span><span class="ss">timeout:</span> <span class="mi">5_000</span><span class="p">,</span> <span class="ss">recv_timeout:</span> <span class="ss">:infinity</span><span class="p">,</span> <span class="ss">pool:</span> <span class="ss">:wallaby_pool</span><span class="p">]</span>
</code></pre></div></div>

<h3 id="process_links1"><code class="language-plaintext highlighter-rouge">process_links/1</code></h3>

<p>With the <code class="language-plaintext highlighter-rouge">session</code> token in hand, we can now start processing our page. The
first step is to find all the links on the page and store them. We can do that
with <code class="language-plaintext highlighter-rouge">Wallaby</code>’s <code class="language-plaintext highlighter-rouge">Query.css/2</code> function. Below you can see that we’re only
looking for links in the <code class="language-plaintext highlighter-rouge">header</code> and <code class="language-plaintext highlighter-rouge">footer</code> elements, which once found,
we iterate through each to store them using <code class="language-plaintext highlighter-rouge">store_link/1</code>.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defp</span> <span class="n">process_links</span><span class="p">(</span><span class="n">session</span><span class="p">)</span> <span class="k">do</span>
  <span class="n">css_selectors</span> <span class="o">=</span> <span class="s2">"header a, footer a"</span>

  <span class="n">session</span>
  <span class="o">|&gt;</span> <span class="no">Browser</span><span class="o">.</span><span class="n">all</span><span class="p">(</span><span class="no">Query</span><span class="o">.</span><span class="n">css</span><span class="p">(</span><span class="n">css_selectors</span><span class="p">,</span> <span class="ss">minimum:</span> <span class="mi">0</span><span class="p">))</span>
  <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">each</span><span class="p">(</span><span class="o">&amp;</span><span class="n">store_link</span><span class="o">/</span><span class="mi">1</span><span class="p">)</span>

  <span class="n">session</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="store_link1"><code class="language-plaintext highlighter-rouge">store_link/1</code></h3>

<p>Even though the <code class="language-plaintext highlighter-rouge">store_link/1</code> function and its associated private functions
take up more space than anything else in the module, it’s relatively simple and
can be broken down into the following steps:</p>

<ol>
  <li>Retrieve the URL from the anchor element passed in</li>
  <li>Compare it against allowed domains</li>
  <li>Push it onto the <code class="language-plaintext highlighter-rouge">URLQueue</code> and <code class="language-plaintext highlighter-rouge">URLRegistry</code> if it’s an allowed domain</li>
  <li>Ignore it if it’s not an allowed domain</li>
</ol>

<p>The <code class="language-plaintext highlighter-rouge">store_link/1</code> function is what starts things off. It accepts an <code class="language-plaintext highlighter-rouge">Element</code>
<code class="language-plaintext highlighter-rouge">struct</code>, retrieves the URL from the <code class="language-plaintext highlighter-rouge">href</code> attribute, and passes that value to
<code class="language-plaintext highlighter-rouge">_store_link/1</code>.</p>

<p><code class="language-plaintext highlighter-rouge">_store_link/1</code> then uses binary matching to filter out URLs that aren’t
allowed. In our case, we’re only looking for URLs under the “iana.org” domain.
If it matches we call <code class="language-plaintext highlighter-rouge">_store_good_link/1</code>, otherwise we print the URL to
<code class="language-plaintext highlighter-rouge">stdout</code> with the label of “Some other link.”</p>

<p>The final step is to call the <code class="language-plaintext highlighter-rouge">_store_good_link/1</code> function. This function
builds a <code class="language-plaintext highlighter-rouge">Map</code> containing two items: a <code class="language-plaintext highlighter-rouge">url</code> and the <code class="language-plaintext highlighter-rouge">module</code> used to crawl
that URL (<code class="language-plaintext highlighter-rouge">module.crawl</code> is used on line 25 of the <code class="language-plaintext highlighter-rouge">Pipeline</code> module.) Next, it
checks to see if that <code class="language-plaintext highlighter-rouge">url_item</code> has already been registered in the
<code class="language-plaintext highlighter-rouge">URLRegistry</code>. If it has, it returns <code class="language-plaintext highlighter-rouge">:ok</code>, otherwise it prints out the
contents, registers the item, and pushes it onto the queue to be crawled.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defp</span> <span class="n">store_link</span><span class="p">(</span><span class="n">element</span><span class="p">)</span> <span class="k">do</span>
  <span class="n">element</span>
  <span class="o">|&gt;</span> <span class="no">Element</span><span class="o">.</span><span class="n">attr</span><span class="p">(</span><span class="s2">"href"</span><span class="p">)</span>
  <span class="o">|&gt;</span> <span class="n">_store_link</span><span class="p">()</span>
<span class="k">end</span>

<span class="k">defp</span> <span class="n">_store_link</span><span class="p">(</span><span class="s2">"https://www.iana.org/"</span> <span class="o">&lt;&gt;</span> <span class="n">_rest</span> <span class="o">=</span> <span class="n">url</span><span class="p">)</span> <span class="k">do</span>
  <span class="n">_store_good_link</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
<span class="k">end</span>

<span class="k">defp</span> <span class="n">_store_link</span><span class="p">(</span><span class="s2">"https://iana.org/"</span> <span class="o">&lt;&gt;</span> <span class="n">_rest</span> <span class="o">=</span> <span class="n">url</span><span class="p">)</span> <span class="k">do</span>
  <span class="n">_store_good_link</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
<span class="k">end</span>

<span class="k">defp</span> <span class="n">_store_link</span><span class="p">(</span><span class="n">url</span><span class="p">)</span> <span class="k">do</span>
  <span class="no">IO</span><span class="o">.</span><span class="n">inspect</span> <span class="n">url</span><span class="p">,</span> <span class="ss">label:</span> <span class="s2">"Some other link"</span>
  <span class="ss">:ok</span>
<span class="k">end</span>

<span class="k">defp</span> <span class="n">_store_good_link</span><span class="p">(</span><span class="n">url</span><span class="p">)</span> <span class="k">do</span>
  <span class="n">url_item</span> <span class="o">=</span> <span class="p">%{</span>
    <span class="ss">url:</span> <span class="n">url</span><span class="p">,</span>
    <span class="ss">module:</span> <span class="bp">__MODULE__</span>
  <span class="p">}</span>

  <span class="k">case</span> <span class="no">URLRegistry</span><span class="o">.</span><span class="n">registered?</span><span class="p">(</span><span class="n">url_item</span><span class="p">)</span> <span class="k">do</span>
    <span class="no">true</span> <span class="o">-&gt;</span>
      <span class="ss">:ok</span>
    <span class="no">false</span> <span class="o">-&gt;</span>
      <span class="no">IO</span><span class="o">.</span><span class="n">inspect</span> <span class="n">url_item</span>
      <span class="no">URLRegistry</span><span class="o">.</span><span class="n">register</span><span class="p">(</span><span class="n">url_item</span><span class="p">)</span>
      <span class="no">URLQueue</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="n">url_item</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="process_page2"><code class="language-plaintext highlighter-rouge">process_page/2</code></h3>

<p>The last step in the Crawler pipeline before we end the session is
<code class="language-plaintext highlighter-rouge">process_page/2</code>. This is where the magic happens. In our example, all we do is
print out that we’re processing the page, but in a “real world” scenario, this
is where you would retrieve data from the page, submit forms, follow links, etc.
I’ll leave it up to you to determine how best to complete this function. You
will, however, need to return the session object in order to successfully end
the session.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defp</span> <span class="n">process_page</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">url</span><span class="p">)</span> <span class="k">do</span>
  <span class="no">IO</span><span class="o">.</span><span class="n">inspect</span> <span class="s2">"Processing page: </span><span class="si">#{</span><span class="n">url</span><span class="si">}</span><span class="s2">"</span>
  <span class="n">session</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="urlregistry">URLRegistry</h2>

<p>The last module to consider from the architecture section, and perhaps a bit
anti-climactic, is the <code class="language-plaintext highlighter-rouge">URLRegistry</code>. It’s a <code class="language-plaintext highlighter-rouge">GenServer</code> that uses a <code class="language-plaintext highlighter-rouge">MapSet</code> to
store a unique list of <code class="language-plaintext highlighter-rouge">url_item</code>s. The two functions in the API you will use
are <code class="language-plaintext highlighter-rouge">register/1</code>, to register a <code class="language-plaintext highlighter-rouge">url_item</code>, and <code class="language-plaintext highlighter-rouge">registered?</code> to determine if a
<code class="language-plaintext highlighter-rouge">url_item</code> has already been crawled.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">URLRegistry</span> <span class="k">do</span>
  <span class="kn">use</span> <span class="no">GenServer</span>

  <span class="k">def</span> <span class="n">start_link</span><span class="p">(</span><span class="n">args</span><span class="p">)</span> <span class="k">do</span>
    <span class="no">GenServer</span><span class="o">.</span><span class="n">start_link</span><span class="p">(</span><span class="bp">__MODULE__</span><span class="p">,</span> <span class="n">args</span><span class="p">,</span> <span class="ss">name:</span> <span class="bp">__MODULE__</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">register</span><span class="p">(</span><span class="n">url_item</span><span class="p">)</span> <span class="k">do</span>
    <span class="no">GenServer</span><span class="o">.</span><span class="n">cast</span><span class="p">(</span><span class="bp">__MODULE__</span><span class="p">,</span> <span class="p">{</span><span class="ss">:register</span><span class="p">,</span> <span class="n">url_item</span><span class="p">})</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">registered?</span><span class="p">(</span><span class="n">url_item</span><span class="p">)</span> <span class="k">do</span>
    <span class="no">GenServer</span><span class="o">.</span><span class="n">call</span><span class="p">(</span><span class="bp">__MODULE__</span><span class="p">,</span> <span class="p">{</span><span class="ss">:registered?</span><span class="p">,</span> <span class="n">url_item</span><span class="p">})</span>
  <span class="k">end</span>

  <span class="nv">@impl</span> <span class="no">GenServer</span>
  <span class="k">def</span> <span class="n">init</span><span class="p">(</span><span class="n">_args</span><span class="p">),</span> <span class="k">do</span><span class="p">:</span> <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="no">MapSet</span><span class="o">.</span><span class="n">new</span><span class="p">()}</span>

  <span class="nv">@impl</span> <span class="no">GenServer</span>
  <span class="k">def</span> <span class="n">handle_cast</span><span class="p">({</span><span class="ss">:register</span><span class="p">,</span> <span class="n">url_item</span><span class="p">},</span> <span class="n">state</span><span class="p">)</span> <span class="k">do</span>
    <span class="p">{</span><span class="ss">:noreply</span><span class="p">,</span> <span class="no">MapSet</span><span class="o">.</span><span class="n">put</span><span class="p">(</span><span class="n">state</span><span class="p">,</span> <span class="n">url_item</span><span class="p">)}</span>
  <span class="k">end</span>

  <span class="nv">@impl</span> <span class="no">GenServer</span>
  <span class="k">def</span> <span class="n">handle_call</span><span class="p">({</span><span class="ss">:registered?</span><span class="p">,</span> <span class="n">url_item</span><span class="p">},</span> <span class="n">_from</span><span class="p">,</span> <span class="n">state</span><span class="p">)</span> <span class="k">do</span>
    <span class="p">{</span><span class="ss">:reply</span><span class="p">,</span> <span class="no">MapSet</span><span class="o">.</span><span class="n">member?</span><span class="p">(</span><span class="n">state</span><span class="p">,</span> <span class="n">url_item</span><span class="p">),</span> <span class="n">state</span><span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This is a very naive approach to keeping track of crawled URLs, but is useful
for demonstration purposes. In a production environment, you might consider an
ETS table or a Redis store with TTL (Time to Live) settings so pages can be
re-crawled after a set period of time.</p>

<h2 id="running-the-crawler">Running the Crawler</h2>

<p>To run the example Crawler, you need to start the <code class="language-plaintext highlighter-rouge">URLQueue</code>, <code class="language-plaintext highlighter-rouge">Pipeline</code>, and
<code class="language-plaintext highlighter-rouge">URLRegistry</code> processes, and then execute the <code class="language-plaintext highlighter-rouge">init/0</code> function for the desired
Crawler. Here’s an example of doing that and the resulting output.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">iex1</span><span class="o">&gt;</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">URLQueue</span><span class="o">.</span><span class="n">start_link</span><span class="p">([])</span>
<span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="c1">#PID&lt;0.252.0&gt;}</span>

<span class="n">iex2</span><span class="o">&gt;</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">Pipeline</span><span class="o">.</span><span class="n">start_link</span><span class="p">([])</span>
<span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="c1">#PID&lt;0.253.0&gt;}</span>

<span class="n">iex3</span><span class="o">&gt;</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">URLRegistry</span><span class="o">.</span><span class="n">start_link</span><span class="p">([])</span>
<span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="c1">#PID&lt;0.254.0&gt;}</span>

<span class="n">iex4</span><span class="o">&gt;</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">Crawlers</span><span class="o">.</span><span class="no">Example</span><span class="o">.</span><span class="n">init</span><span class="p">()</span>
<span class="ss">:ok</span>

<span class="o">...</span><span class="k">after</span> <span class="n">a</span> <span class="n">few</span> <span class="n">moments</span> <span class="n">of</span> <span class="n">waiting</span><span class="o">...</span>

<span class="p">%{</span>
  <span class="ss">module:</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">Crawlers</span><span class="o">.</span><span class="no">Example</span><span class="p">,</span>
  <span class="ss">url:</span> <span class="s2">"https://www.iana.org/"</span>
<span class="p">}</span>
<span class="p">%{</span>
  <span class="ss">module:</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">Crawlers</span><span class="o">.</span><span class="no">Example</span><span class="p">,</span>
  <span class="ss">url:</span> <span class="s2">"https://www.iana.org/domains"</span>
<span class="p">}</span>
<span class="p">%{</span>
  <span class="ss">module:</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">Crawlers</span><span class="o">.</span><span class="no">Example</span><span class="p">,</span>
  <span class="ss">url:</span> <span class="s2">"https://www.iana.org/protocols"</span>
<span class="p">}</span>

<span class="o">...</span>

<span class="no">Some</span> <span class="n">other</span> <span class="ss">link:</span> <span class="s2">"https://pti.icann.org/"</span>
<span class="no">Some</span> <span class="n">other</span> <span class="ss">link:</span> <span class="s2">"https://www.icann.org/"</span>
<span class="no">Some</span> <span class="n">other</span> <span class="ss">link:</span> <span class="s2">"https://www.icann.org/privacy/policy"</span>
<span class="no">Some</span> <span class="n">other</span> <span class="ss">link:</span> <span class="s2">"https://www.icann.org/privacy/tos"</span>
<span class="s2">"Processing page: https://www.iana.org/help/example-domains"</span>
<span class="no">Some</span> <span class="n">other</span> <span class="ss">link:</span> <span class="s2">"https://pti.icann.org/"</span>
<span class="no">Some</span> <span class="n">other</span> <span class="ss">link:</span> <span class="s2">"https://www.icann.org/"</span>
<span class="no">Some</span> <span class="n">other</span> <span class="ss">link:</span> <span class="s2">"https://www.icann.org/privacy/policy"</span>
<span class="no">Some</span> <span class="n">other</span> <span class="ss">link:</span> <span class="s2">"https://www.icann.org/privacy/tos"</span>
<span class="s2">"Processing page: https://www.iana.org/about/excellence"</span>
<span class="no">Some</span> <span class="n">other</span> <span class="ss">link:</span> <span class="s2">"https://pti.icann.org/"</span>

</code></pre></div></div>

<p>In a supervised app, you’d just need to add those processes to your
<code class="language-plaintext highlighter-rouge">application.ex</code> file.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="n">start</span><span class="p">(</span><span class="n">_type</span><span class="p">,</span> <span class="n">_args</span><span class="p">)</span> <span class="k">do</span>
  <span class="n">children</span> <span class="o">=</span> <span class="p">[</span>
    <span class="p">{</span><span class="no">MyApp</span><span class="o">.</span><span class="no">URLQueue</span><span class="p">,</span> <span class="p">[]},</span>
    <span class="p">{</span><span class="no">MyApp</span><span class="o">.</span><span class="no">Pipeline</span><span class="p">,</span> <span class="p">[]},</span>
    <span class="p">{</span><span class="no">MyApp</span><span class="o">.</span><span class="no">URLRegistry</span><span class="p">,</span> <span class="p">[]}</span>
  <span class="p">]</span>

  <span class="n">opts</span> <span class="o">=</span> <span class="p">[</span><span class="ss">strategy:</span> <span class="ss">:one_for_one</span><span class="p">,</span> <span class="ss">name:</span> <span class="no">Glutton</span><span class="o">.</span><span class="no">Supervisor</span><span class="p">]</span>
  <span class="no">Supervisor</span><span class="o">.</span><span class="n">start_link</span><span class="p">(</span><span class="n">children</span><span class="p">,</span> <span class="n">opts</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="consider-also">Consider Also…</h2>

<p>Before we wrap this up there are a few things you should also consider before
building your own crawler(s).</p>

<h3 id="be-a-good-netizen">Be a Good Netizen</h3>

<p>Crawling a webpage requires resources from the hosting site and you’ll want to
make sure you don’t overload that site consuming their data. To that end, the
authors of the excellent <a href="https://github.com/elixir-crawly/crawly">Crawly</a>
library have provided four items which define a polite crawler:</p>

<ol>
  <li>A polite crawler respects <code class="language-plaintext highlighter-rouge">robots.txt</code>.</li>
  <li>A polite crawler never degrades a website’s performance.</li>
  <li>A polite crawler identifies its creator with contact information.</li>
  <li>A polite crawler is not a pain in the buttocks of system administrators.</li>
</ol>

<h3 id="poolboy">Poolboy</h3>

<p>In the <code class="language-plaintext highlighter-rouge">Example</code> crawler we initiate and end a Wallaby session in the <code class="language-plaintext highlighter-rouge">crawl/1</code>
pipeline. But what happens if an error occurs before the pipeline finishes?
Well, you end up with an unfinished session which can eat up resources. A better
solution would be to use <a href="https://github.com/devinus/poolboy">poolboy</a> to manage
sessions.</p>

<p>For brevity’s sake, I didn’t include poolboy’s use in this article, but I have
another article tackling its use: <a href="https://samuelmullen.com/articles/elixir-poolboy-and-littles-law">Elixir, Poolboy, and Little’s
Law</a>.</p>

<h3 id="build-crawlers-with-protocols-or-behaviours">Build Crawlers with Protocols or Behaviours</h3>

<p>If you end up with multiple Crawlers, you might find it beneficial to use either
a Protocol or Behaviour to reduce code duplication. Again, I didn’t include that
here for the sake of brevity, but what a great idea for an article.</p>

<h3 id="using-floki">Using Floki</h3>

<p>Wallaby’s good at retrieving HTML from a page, but if you need more control over
parsing it, you can output the page source with <code class="language-plaintext highlighter-rouge">Wallaby.Browser.page_source/1</code>
and use <a href="https://github.com/philss/floki">Floki</a> to get at the content you want.</p>

<h2 id="wrapping-up">Wrapping Up</h2>

<p>Crawling the web, like traversing a file system, is a kind of recursive process:
you start from a single page, collect all the links from it, crawl the next
page, collect the links, and so on until you’ve crawled everything you need.
It’s a perfect problem to solve with Broadway, thanks to its concurrent
processing, use of back-pressure, and rate-limiting. When combined with Wallaby
and its use of headless browsers, you’re no longer limited to traditional
HTML-only pages, but can crawl SPA sites as well.</p>

<p>All the code above and from my <a href="https://samuelmullen.com/articles/building-custom-producers-with-elixirs-broadway">Building Custom Producers with Elixir’s
Broadway</a>
article can be found in my <a href="https://github.com/samullen/glutton">Glutton</a> GitHub
repo.</p>]]></content><author><name></name></author><category term="elixir" /><category term="broadway" /><category term="wallaby" /><category term="web crawling" /><summary type="html"><![CDATA[Discover how to combine Broadway’s powerful pipelines with Wallaby’s browser automation to create a scalable, concurrent Elixir web crawler.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://samuelmullen.com/assets/images/crawling_the_web/elixir_crawl.png" /><media:content medium="image" url="https://samuelmullen.com/assets/images/crawling_the_web/elixir_crawl.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Building Custom Producers with Elixir’s Broadway</title><link href="https://samuelmullen.com/articles/building-custom-producers-with-elixirs-broadway" rel="alternate" type="text/html" title="Building Custom Producers with Elixir’s Broadway" /><published>2025-10-23T05:00:00+00:00</published><updated>2025-10-23T05:00:00+00:00</updated><id>https://samuelmullen.com/articles/building-custom-producers-with-elixirs-broadway</id><content type="html" xml:base="https://samuelmullen.com/articles/building-custom-producers-with-elixirs-broadway"><![CDATA[<aside class="panel panel-default pull-right col-md-4">
If you're not familiar with Broadway, you should read my article,
<a href="https://samuelmullen.com/articles/understanding-elixirs-broadway">Understanding Elixir's Broadway</a> first.
</aside>

<p>The thing about <a href="https://github.com/elixir-lang/elixir">Elixir</a>’s
<a href="https://github.com/dashbitco/broadway">Broadway</a> is that once you’ve used it,
you start seeing opportunities to use everywhere. We used it heavily at my
previous job, and it was my go-to tool for any continuous stream of data that
required concurrent processing.</p>

<p>Recently I decided to build a web crawler/scraper to pull in data for my side
project, <a href="https://makerplans.io">Makerplans</a>. I’d been using
<a href="https://github.com/elixir-crawly/crawly">Crawly</a> with some success, but so many
sites are SPAs that it makes libraries like Crawly, which rely on full page
reloads, less useful.</p>

<p>I’ll write more about building my crawler/scraper,
<a href="https://github.com/samullen/glutton">Glutton</a>, in a future article. For now, I want to focus on the first problem I ran into: getting custom Broadway producers to work.</p>

<h2 id="v1">v1</h2>

<p>The <a href="https://dashbit.co">Dashbit</a> team did a great job documenting Broadway, and
they even have a page describing how to build
<a href="https://hexdocs.pm/broadway/custom-producers.html">Custom Producers</a>. It’s what
I referenced while building the first version of Glutton’s Producer (shown
below). On the surface, this appears to be doing everything the custom Producer
example is doing, but as we’ll see, there’s a problem.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">URLProducer</span> <span class="k">do</span>
  <span class="kn">use</span> <span class="no">GenStage</span>

  <span class="n">alias</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">URLQueue</span>

  <span class="k">def</span> <span class="n">start_link</span><span class="p">(</span><span class="n">args</span><span class="p">)</span> <span class="k">do</span>
    <span class="no">GenStage</span><span class="o">.</span><span class="n">start_link</span><span class="p">(</span><span class="bp">__MODULE__</span><span class="p">,</span> <span class="n">args</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="nv">@impl</span> <span class="no">GenStage</span>
  <span class="k">def</span> <span class="n">init</span><span class="p">(</span><span class="n">_args</span><span class="p">),</span> <span class="k">do</span><span class="p">:</span> <span class="p">{</span><span class="ss">:producer</span><span class="p">,</span> <span class="p">[]}</span>

  <span class="nv">@impl</span> <span class="no">GenStage</span>
  <span class="k">def</span> <span class="n">handle_demand</span><span class="p">(</span><span class="n">demand</span><span class="p">,</span> <span class="n">state</span><span class="p">)</span> <span class="ow">when</span> <span class="n">demand</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="k">do</span>
    <span class="n">urls</span> <span class="o">=</span> <span class="no">URLQueue</span><span class="o">.</span><span class="n">pop</span><span class="p">(</span><span class="n">demand</span><span class="p">)</span>

    <span class="p">{</span><span class="ss">:noreply</span><span class="p">,</span> <span class="n">urls</span><span class="p">,</span> <span class="p">[]}</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>You’ll notice first that the <code class="language-plaintext highlighter-rouge">URLProducer</code> uses the
<a href="https://github.com/elixir-lang/gen_stage">GenStage</a> behavior. It’s very
similar to the <code class="language-plaintext highlighter-rouge">GenServer</code> behavior with the exception of <code class="language-plaintext highlighter-rouge">init/1</code>’s return
value and the <code class="language-plaintext highlighter-rouge">handle_demand/2</code> functions. Whereas a <code class="language-plaintext highlighter-rouge">GenServer</code>’s <code class="language-plaintext highlighter-rouge">init/1</code> will
return an <code class="language-plaintext highlighter-rouge">{:ok, state}</code> tuple, Broadway expects its producers to return
<code class="language-plaintext highlighter-rouge">{:producer, state}</code>, which is what we do above.</p>

<p><code class="language-plaintext highlighter-rouge">handle_demand/2</code> is very similar to <code class="language-plaintext highlighter-rouge">GenServer</code>’s <code class="language-plaintext highlighter-rouge">handle_call/3</code> or
<code class="language-plaintext highlighter-rouge">handle_cast/2</code> functions, except it, uh…handles…demand from consumers,
which is what your main Broadway module is.</p>

<p>Speaking of the Broadway consumer, let’s make that:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">Pipeline</span> <span class="k">do</span>
  <span class="kn">use</span> <span class="no">Broadway</span>

  <span class="n">alias</span> <span class="no">Broadway</span><span class="o">.</span><span class="no">Message</span>

  <span class="k">def</span> <span class="n">start_link</span><span class="p">(</span><span class="n">_opts</span><span class="p">)</span> <span class="k">do</span>
    <span class="no">Broadway</span><span class="o">.</span><span class="n">start_link</span><span class="p">(</span><span class="bp">__MODULE__</span><span class="p">,</span>
      <span class="ss">name:</span> <span class="bp">__MODULE__</span><span class="p">,</span>
      <span class="ss">producer:</span> <span class="p">[</span>
        <span class="ss">module:</span> <span class="p">{</span><span class="no">MyApp</span><span class="o">.</span><span class="no">URLProducer</span><span class="p">,</span> <span class="p">[]},</span>
        <span class="ss">transformer:</span> <span class="p">{</span><span class="bp">__MODULE__</span><span class="p">,</span> <span class="ss">:transform</span><span class="p">,</span> <span class="p">[]},</span>
        <span class="ss">concurrency:</span> <span class="mi">1</span>
      <span class="p">],</span>
      <span class="ss">processors:</span> <span class="p">[</span>
        <span class="ss">default:</span> <span class="p">[</span>
          <span class="ss">concurrency:</span> <span class="mi">2</span><span class="p">,</span>
          <span class="ss">min_demand:</span> <span class="mi">1</span><span class="p">,</span>
          <span class="ss">max_demand:</span> <span class="mi">2</span>
        <span class="p">]</span>
      <span class="p">]</span>
    <span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">handle_message</span><span class="p">(</span><span class="ss">:default</span><span class="p">,</span> <span class="p">%</span><span class="no">Message</span><span class="p">{</span><span class="ss">data:</span> <span class="n">url</span><span class="p">}</span> <span class="o">=</span> <span class="n">message</span><span class="p">,</span> <span class="n">_context</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">message</span>
    <span class="o">|&gt;</span> <span class="no">IO</span><span class="o">.</span><span class="n">inspect</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">transform</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">_opts</span><span class="p">)</span> <span class="k">do</span>
    <span class="p">%</span><span class="no">Broadway</span><span class="o">.</span><span class="no">Message</span><span class="p">{</span>
      <span class="ss">data:</span> <span class="n">url</span><span class="p">,</span>
      <span class="ss">acknowledger:</span> <span class="no">Broadway</span><span class="o">.</span><span class="no">NoopAcknowledger</span><span class="o">.</span><span class="n">init</span><span class="p">()</span>
    <span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">ack</span><span class="p">(</span><span class="n">_ref</span><span class="p">,</span> <span class="n">_successes</span><span class="p">,</span> <span class="n">_failures</span><span class="p">)</span> <span class="k">do</span>
    <span class="ss">:ok</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<aside class="panel panel-default pull-right col-md-4">
  ProTip: Not setting concurrency will default to <code>System.schedulers_online()
  * 2</code>. On my machine that's 16 and it absolutely crushed it as a web
  crawler once I had everything running.

  <img src="https://samuelmullen.com/assets/images/custom_broadway_producers/load.png" class="img-thumbnail img-responsive" alt="load" title="Load" />
</aside>

<p>This is a standard Broadway consumer, so if you’ve used Broadway before, this
should look familiar. I kept the concurrency and demand low because each Glutton
process runs a headless Chrome browser, and processing is already slow enough
that higher demand isn’t necessary.</p>

<p>Lastly, we need a “queue” from which our custom producer can draw. We can do
that with a simple <code class="language-plaintext highlighter-rouge">GenServer</code>.</p>

<div class="clearfix"></div>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">URLQueue</span> <span class="k">do</span>
  <span class="kn">use</span> <span class="no">GenServer</span>

  <span class="k">def</span> <span class="n">start_link</span><span class="p">(</span><span class="n">args</span><span class="p">)</span> <span class="k">do</span>
    <span class="no">GenServer</span><span class="o">.</span><span class="n">start_link</span><span class="p">(</span><span class="bp">__MODULE__</span><span class="p">,</span> <span class="n">args</span><span class="p">,</span> <span class="ss">name:</span> <span class="bp">__MODULE__</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">push</span><span class="p">(</span><span class="n">url</span><span class="p">)</span> <span class="k">do</span>
    <span class="no">GenServer</span><span class="o">.</span><span class="n">cast</span><span class="p">(</span><span class="bp">__MODULE__</span><span class="p">,</span> <span class="p">{</span><span class="ss">:push</span><span class="p">,</span> <span class="n">url</span><span class="p">})</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">pop</span><span class="p">(</span><span class="n">count</span><span class="p">)</span> <span class="k">do</span>
    <span class="no">GenServer</span><span class="o">.</span><span class="n">call</span><span class="p">(</span><span class="bp">__MODULE__</span><span class="p">,</span> <span class="p">{</span><span class="ss">:pop</span><span class="p">,</span> <span class="n">count</span><span class="p">})</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">list</span><span class="p">()</span> <span class="k">do</span>
    <span class="no">GenServer</span><span class="o">.</span><span class="n">call</span><span class="p">(</span><span class="bp">__MODULE__</span><span class="p">,</span> <span class="ss">:list</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="nv">@impl</span> <span class="no">GenServer</span>
  <span class="k">def</span> <span class="n">init</span><span class="p">(</span><span class="n">_args</span><span class="p">),</span> <span class="k">do</span><span class="p">:</span> <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="p">[]}</span>

  <span class="nv">@impl</span> <span class="no">GenServer</span>
  <span class="k">def</span> <span class="n">handle_cast</span><span class="p">({</span><span class="ss">:push</span><span class="p">,</span> <span class="n">url</span><span class="p">},</span> <span class="n">state</span><span class="p">)</span> <span class="k">do</span>
    <span class="p">{</span><span class="ss">:noreply</span><span class="p">,</span> <span class="p">[</span><span class="n">url</span> <span class="o">|</span> <span class="n">state</span><span class="p">]}</span>
  <span class="k">end</span>

  <span class="nv">@impl</span> <span class="no">GenServer</span>
  <span class="k">def</span> <span class="n">handle_call</span><span class="p">({</span><span class="ss">:pop</span><span class="p">,</span> <span class="n">count</span><span class="p">},</span> <span class="n">_from</span><span class="p">,</span> <span class="n">state</span><span class="p">)</span> <span class="k">do</span>
    <span class="p">{</span><span class="n">urls</span><span class="p">,</span> <span class="n">new_state</span><span class="p">}</span> <span class="o">=</span> <span class="no">Enum</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="n">state</span><span class="p">,</span> <span class="n">count</span><span class="p">)</span>
    <span class="p">{</span><span class="ss">:reply</span><span class="p">,</span> <span class="n">urls</span><span class="p">,</span> <span class="n">new_state</span><span class="p">}</span>
  <span class="k">end</span>

  <span class="nv">@impl</span> <span class="no">GenServer</span>
  <span class="k">def</span> <span class="n">handle_call</span><span class="p">(</span><span class="ss">:list</span><span class="p">,</span> <span class="n">_from</span><span class="p">,</span> <span class="n">state</span><span class="p">)</span> <span class="k">do</span>
    <span class="p">{</span><span class="ss">:reply</span><span class="p">,</span> <span class="n">state</span><span class="p">,</span> <span class="n">state</span><span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<aside class="panel panel-default pull-right col-md-4">
  ProTip: In a real-world project, you'd want to use Erlang's
  <code>:queue</code> module instead of a <code>List</code>.
</aside>

<p>It’s a straightforward <code class="language-plaintext highlighter-rouge">GenServer</code>. <code class="language-plaintext highlighter-rouge">state</code> is just a list to which we can add
URLs (<code class="language-plaintext highlighter-rouge">push/1</code>), remove the next one (<code class="language-plaintext highlighter-rouge">pop/0</code>), or get the current contents of
the queue (<code class="language-plaintext highlighter-rouge">list/0</code>).</p>

<div class="clearfix"></div>

<h3 id="running-v1">Running v1</h3>

<p>If you were to start these processes up…</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>iex(1)&gt; MyApp.URLQueue.start_link([])
{:ok, #PID&lt;0.196.0&gt;}
iex(2)&gt; MyApp.Pipeline.start_link([])
{:ok, #PID&lt;0.198.0&gt;}
</code></pre></div></div>

<p>…and add items to the queue…</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>iex(3)&gt; MyApp.URLQueue.push("http://example.com")
:ok
iex(4)&gt; MyApp.URLQueue.push("http://example.com")
:ok
iex(5)&gt; MyApp.URLQueue.push("http://example.com")
:ok
iex(6)&gt; MyApp.URLQueue.push("http://example.com")
:ok
iex(7)&gt; MyApp.URLQueue.push("http://example.com")
:ok
</code></pre></div></div>

<p>…you’d notice that…uh…nothing happens.</p>

<p>Hmmm. Okay. What happens if you add items to the queue before starting the
Broadway consumer?</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">iex</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="o">&gt;</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">URLQueue</span><span class="o">.</span><span class="n">start_link</span><span class="p">([])</span>
<span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="c1">#PID&lt;0.196.0&gt;}</span>
<span class="n">iex</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span><span class="o">&gt;</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">URLQueue</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="s2">"http://example.com"</span><span class="p">)</span>
<span class="ss">:ok</span>
<span class="n">iex</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span><span class="o">&gt;</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">URLQueue</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="s2">"http://example.com"</span><span class="p">)</span>
<span class="ss">:ok</span>
<span class="n">iex</span><span class="p">(</span><span class="mi">4</span><span class="p">)</span><span class="o">&gt;</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">Pipeline</span><span class="o">.</span><span class="n">start_link</span><span class="p">([])</span>
<span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="c1">#PID&lt;0.198.0&gt;}</span>
</code></pre></div></div>

<p>Now you’ll see…</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">%</span><span class="no">Broadway</span><span class="o">.</span><span class="no">Message</span><span class="p">{</span>
  <span class="ss">data:</span> <span class="s2">"http://example.com"</span><span class="p">,</span>
  <span class="ss">metadata:</span> <span class="p">%{},</span>
  <span class="ss">acknowledger:</span> <span class="p">{</span><span class="no">Broadway</span><span class="o">.</span><span class="no">NoopAcknowledger</span><span class="p">,</span> <span class="no">nil</span><span class="p">,</span> <span class="no">nil</span><span class="p">},</span>
  <span class="ss">batcher:</span> <span class="ss">:default</span><span class="p">,</span>
  <span class="ss">batch_key:</span> <span class="ss">:default</span><span class="p">,</span>
  <span class="ss">batch_mode:</span> <span class="ss">:bulk</span><span class="p">,</span>
  <span class="ss">status:</span> <span class="ss">:ok</span>
<span class="p">}</span>
<span class="p">%</span><span class="no">Broadway</span><span class="o">.</span><span class="no">Message</span><span class="p">{</span>
  <span class="ss">data:</span> <span class="s2">"http://example.com"</span><span class="p">,</span>
  <span class="ss">metadata:</span> <span class="p">%{},</span>
  <span class="ss">acknowledger:</span> <span class="p">{</span><span class="no">Broadway</span><span class="o">.</span><span class="no">NoopAcknowledger</span><span class="p">,</span> <span class="no">nil</span><span class="p">,</span> <span class="no">nil</span><span class="p">},</span>
  <span class="ss">batcher:</span> <span class="ss">:default</span><span class="p">,</span>
  <span class="ss">batch_key:</span> <span class="ss">:default</span><span class="p">,</span>
  <span class="ss">batch_mode:</span> <span class="ss">:bulk</span><span class="p">,</span>
  <span class="ss">status:</span> <span class="ss">:ok</span>
<span class="p">}</span>
</code></pre></div></div>

<p>…but if you were to add more URLs to the queue, nothing else happens. What
gives?</p>

<h3 id="the-problem-in-v1">The Problem in v1</h3>

<p>As mentioned at the beginning of this section, there was a problem in the way
I initially wrote the producer: I followed the documentation. While the Broadway
documentation for
<a href="https://hexdocs.pm/broadway/custom-producers.html">Custom Producers</a> is useful,
it falls short in describing how to handle instances where data isn’t always
available to retrieve (i.e. what happens when you’ve processed everything?) In
their <code class="language-plaintext highlighter-rouge">Counter</code> example, they provided the following function for handling
demand from the Broadway Consumer:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="n">handle_demand</span><span class="p">(</span><span class="n">demand</span><span class="p">,</span> <span class="n">counter</span><span class="p">)</span> <span class="ow">when</span> <span class="n">demand</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="k">do</span>
  <span class="n">events</span> <span class="o">=</span> <span class="no">Enum</span><span class="o">.</span><span class="n">to_list</span><span class="p">(</span><span class="n">counter</span><span class="o">..</span><span class="n">counter</span><span class="o">+</span><span class="n">demand</span><span class="o">-</span><span class="mi">1</span><span class="p">)</span>
  <span class="p">{</span><span class="ss">:noreply</span><span class="p">,</span> <span class="n">events</span><span class="p">,</span> <span class="n">counter</span> <span class="o">+</span> <span class="n">demand</span><span class="p">}</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Notice that when there is demand, this function returns both a list of
integers—which is always available since it generates the list
itself—and updates state. There’s never a scenario when this function
won’t be able to handle demand. But in our app, it’s likely, and even expected,
to run out of URLs to crawl. What’s going on?</p>

<p>In response to this very problem brought up in
<a href="https://github.com/elixir-lang/gen_stage/issues/80">a GenStage issue</a>, José
explained that if a consumer (our <code class="language-plaintext highlighter-rouge">Pipeline</code>) sends demand and the producer
(<code class="language-plaintext highlighter-rouge">URLProducer</code>) can’t serve that demand immediately, the producer should “store”
that demand until it’s able to fulfill it.</p>

<p>The mistake I made was thinking the consumer would continually ask its producers
for data. It doesn’t. It asks once and expects its producers to fulfill that
demand whenever they’re able. Because our <code class="language-plaintext highlighter-rouge">handle_demand/2</code> function only runs
when a request is made from the consumer, and the consumer is only going to ask
once until demand is fulfilled, our app stalled out.</p>

<p>The way to fix that is to make our <code class="language-plaintext highlighter-rouge">URLProducer</code> keep checking the <code class="language-plaintext highlighter-rouge">URLQueue</code>
for data.</p>

<h2 id="v2">v2</h2>

<p>Here’s the second version of <code class="language-plaintext highlighter-rouge">URLProducer</code>.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">URLProducer</span> <span class="k">do</span>
  <span class="kn">use</span> <span class="no">GenStage</span>

  <span class="n">alias</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">URLQueue</span>

  <span class="nv">@receive_interval</span> <span class="mi">5_000</span>

  <span class="k">def</span> <span class="n">start_link</span><span class="p">(</span><span class="n">args</span><span class="p">)</span> <span class="k">do</span>
    <span class="no">GenStage</span><span class="o">.</span><span class="n">start_link</span><span class="p">(</span><span class="bp">__MODULE__</span><span class="p">,</span> <span class="n">args</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="nv">@impl</span> <span class="no">GenStage</span>
  <span class="k">def</span> <span class="n">init</span><span class="p">(</span><span class="n">_args</span><span class="p">),</span> <span class="k">do</span><span class="p">:</span> <span class="p">{</span><span class="ss">:producer</span><span class="p">,</span> <span class="p">%{</span><span class="ss">demand:</span> <span class="mi">0</span><span class="p">}}</span>

  <span class="nv">@impl</span> <span class="no">GenStage</span>
  <span class="k">def</span> <span class="n">handle_demand</span><span class="p">(</span><span class="n">incoming_demand</span><span class="p">,</span> <span class="n">state</span><span class="p">)</span> <span class="ow">when</span> <span class="n">incoming_demand</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="k">do</span>
    <span class="n">schedule_receive_messages</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>

    <span class="p">{</span><span class="ss">:noreply</span><span class="p">,</span> <span class="p">[],</span> <span class="p">%{</span><span class="n">state</span> <span class="o">|</span> <span class="ss">demand:</span> <span class="n">state</span><span class="o">.</span><span class="n">demand</span> <span class="o">+</span> <span class="n">incoming_demand</span><span class="p">}}</span>
  <span class="k">end</span>

  <span class="nv">@impl</span> <span class="no">GenStage</span>
  <span class="k">def</span> <span class="n">handle_info</span><span class="p">(</span><span class="ss">:receive_messages</span><span class="p">,</span> <span class="n">state</span><span class="p">)</span> <span class="k">do</span>
    <span class="k">case</span> <span class="no">URLQueue</span><span class="o">.</span><span class="n">pop</span><span class="p">(</span><span class="n">state</span><span class="o">.</span><span class="n">demand</span><span class="p">)</span> <span class="k">do</span>
      <span class="n">urls</span> <span class="ow">when</span> <span class="n">is_list</span><span class="p">(</span><span class="n">urls</span><span class="p">)</span> <span class="ow">and</span> <span class="n">length</span><span class="p">(</span><span class="n">urls</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="o">-&gt;</span>
        <span class="p">{</span><span class="ss">:noreply</span><span class="p">,</span> <span class="n">urls</span><span class="p">,</span> <span class="p">%{</span><span class="ss">demand:</span> <span class="n">state</span><span class="o">.</span><span class="n">demand</span> <span class="o">-</span> <span class="n">length</span><span class="p">(</span><span class="n">urls</span><span class="p">)}}</span>

      <span class="p">[]</span> <span class="o">-&gt;</span>
        <span class="n">schedule_receive_messages</span><span class="p">(</span><span class="nv">@receive_interval</span><span class="p">)</span>
        <span class="p">{</span><span class="ss">:noreply</span><span class="p">,</span> <span class="p">[],</span> <span class="n">state</span><span class="p">}</span>
    <span class="k">end</span>

  <span class="k">end</span>

  <span class="k">defp</span> <span class="n">schedule_receive_messages</span><span class="p">(</span><span class="n">interval</span><span class="p">)</span> <span class="k">do</span>
    <span class="no">Process</span><span class="o">.</span><span class="n">send_after</span><span class="p">(</span><span class="n">self</span><span class="p">(),</span> <span class="ss">:receive_messages</span><span class="p">,</span> <span class="n">interval</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>There are several changes of which to take note. The first is state. Previously,
we were just passing around an empty list, because we were completely handling
demand on each request. Now we want to keep track of demand and to do so, we’ll
store it in a map. We could keep track of it as an integer, but in most cases,
you’ll also want to keep track of a buffer of data. For example:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">%{</span><span class="ss">urls:</span> <span class="p">[],</span> <span class="ss">demand:</span> <span class="mi">2</span><span class="p">}</span>
</code></pre></div></div>

<p>The next thing to notice is that instead of retrieving URLs from the queue in
<code class="language-plaintext highlighter-rouge">handle_demand/2</code>, we’re calling <code class="language-plaintext highlighter-rouge">schedule_receive_messages/1</code>. This
function— along with <code class="language-plaintext highlighter-rouge">handle_info/2</code> which handles the sent message,
<code class="language-plaintext highlighter-rouge">:receive_messages</code>— sets up an asynchronous loop to continually pull data from
<code class="language-plaintext highlighter-rouge">URLQueue</code>. It responds with data if it’s able to fulfill demand; otherwise, it
schedules itself to check again in five seconds (<code class="language-plaintext highlighter-rouge">@receive_interval</code>.) Without
<code class="language-plaintext highlighter-rouge">schedule_receive_messages/1</code>, and <code class="language-plaintext highlighter-rouge">Process.send_after/3</code>, our producer would
stay idle after the first time it was unable to address demand. Using it allows
the producer to continuously check for new work asynchronously.</p>

<p>Everything else remains the same. If we were to launch our app again and add
URLs to the queue, we’d see that our <code class="language-plaintext highlighter-rouge">Pipeline</code> consumer outputs data within
five seconds of data getting added to the <code class="language-plaintext highlighter-rouge">URLQueue</code>.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">iex</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="o">&gt;</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">URLQueue</span><span class="o">.</span><span class="n">start_link</span><span class="p">([])</span>
<span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="c1">#PID&lt;0.196.0&gt;}</span>
<span class="n">iex</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span><span class="o">&gt;</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">Pipeline</span><span class="o">.</span><span class="n">start_link</span><span class="p">([])</span>
<span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="c1">#PID&lt;0.198.0&gt;}</span>
<span class="n">iex</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span><span class="o">&gt;</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">URLQueue</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="s2">"http://example.com"</span><span class="p">)</span>
<span class="ss">:ok</span>
<span class="n">iex</span><span class="p">(</span><span class="mi">4</span><span class="p">)</span><span class="o">&gt;</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">URLQueue</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="s2">"http://example.com"</span><span class="p">)</span>
<span class="ss">:ok</span>

<span class="p">%</span><span class="no">Broadway</span><span class="o">.</span><span class="no">Message</span><span class="p">{</span>
  <span class="ss">data:</span> <span class="s2">"http://example.com"</span><span class="p">,</span>
  <span class="ss">metadata:</span> <span class="p">%{},</span>
  <span class="ss">acknowledger:</span> <span class="p">{</span><span class="no">Broadway</span><span class="o">.</span><span class="no">NoopAcknowledger</span><span class="p">,</span> <span class="no">nil</span><span class="p">,</span> <span class="no">nil</span><span class="p">},</span>
  <span class="ss">batcher:</span> <span class="ss">:default</span><span class="p">,</span>
  <span class="ss">batch_key:</span> <span class="ss">:default</span><span class="p">,</span>
  <span class="ss">batch_mode:</span> <span class="ss">:bulk</span><span class="p">,</span>
  <span class="ss">status:</span> <span class="ss">:ok</span>
<span class="p">}</span>
<span class="p">%</span><span class="no">Broadway</span><span class="o">.</span><span class="no">Message</span><span class="p">{</span>
  <span class="ss">data:</span> <span class="s2">"http://example.com"</span><span class="p">,</span>
  <span class="ss">metadata:</span> <span class="p">%{},</span>
  <span class="ss">acknowledger:</span> <span class="p">{</span><span class="no">Broadway</span><span class="o">.</span><span class="no">NoopAcknowledger</span><span class="p">,</span> <span class="no">nil</span><span class="p">,</span> <span class="no">nil</span><span class="p">},</span>
  <span class="ss">batcher:</span> <span class="ss">:default</span><span class="p">,</span>
  <span class="ss">batch_key:</span> <span class="ss">:default</span><span class="p">,</span>
  <span class="ss">batch_mode:</span> <span class="ss">:bulk</span><span class="p">,</span>
  <span class="ss">status:</span> <span class="ss">:ok</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The Broadway library is one of Elixir’s unsung heroes. It’s lightweight, easy to
use, and its processing capabilities are unparalleled (Because they run
concurrently. Get it? Ugh, that was so dumb.) Once you’ve figured out how to use
it to solve one problem, you start seeing lots of places where Broadway is a
good fit, but some of those problems are going to require a custom Producer.</p>

<p>As we’ve seen, custom producers are easy to build, but unless they themselves
are the data generators, it can be a little tricky to keep them providing data
to the Broadway consumer. To get around that, we used a combination of
<code class="language-plaintext highlighter-rouge">Process.send_after/3</code> and <code class="language-plaintext highlighter-rouge">GenStage</code>’s <code class="language-plaintext highlighter-rouge">handle_info/2</code>.</p>

<p>I kept the code in this article as simple as possible. To see examples of more
robust solutions, I encourage you to look at the code for both the
<a href="https://hexdocs.pm/broadway/introduction.html#official-producers">“Official” and “Off-Broadway” producers</a>.</p>

<p>In the next article, we’ll build on this foundation and use Wallaby to start crawling websites.</p>]]></content><author><name></name></author><category term="elixir" /><category term="broadway" /><summary type="html"><![CDATA[A practical guide to building custom Broadway producers in Elixir using GenStage. Understand how demand works, how to prevent your pipeline from stalling, and how to use Process.send_after/3 to keep data flowing between your producer and Broadway consumer.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://samuelmullen.com/assets/images/custom_broadway_producers/broadway_flow.png" /><media:content medium="image" url="https://samuelmullen.com/assets/images/custom_broadway_producers/broadway_flow.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The Ivy Lee Method: A 100-Year-Old Productivity Hack That Still Works</title><link href="https://samuelmullen.com/articles/the-ivy-lee-method" rel="alternate" type="text/html" title="The Ivy Lee Method: A 100-Year-Old Productivity Hack That Still Works" /><published>2025-08-20T05:00:00+00:00</published><updated>2025-08-20T05:00:00+00:00</updated><id>https://samuelmullen.com/articles/the-ivy-lee-method</id><content type="html" xml:base="https://samuelmullen.com/articles/the-ivy-lee-method"><![CDATA[<p><img src="https://samuelmullen.com/assets/images/ivy-lee-method/ivy-lee-tasks.png" class="img-thumbnail img-right" /></p>

<p>This idea isn’t original. In fact it’s over 100 years old. It’s a productivity
method created by a man named Ivy Lee when he was asked by Charles M. Schwab to
help him “increase the efficiency of his team and discover better ways to get
things done.” (<a href="https://jamesclear.com/ivy-lee">The Ivy Lee Method - James Clear</a>)</p>

<h2 id="what-it-is">What it is</h2>

<p>The Ivy Lee Method can be summarized like this:</p>

<ol>
  <li>At the end of each workday, write down the most important tasks you need to
complete the next day (no more than six).</li>
  <li>Prioritize those tasks in order of importance.</li>
  <li>The next day, start with the first task and work on it until it’s finished
before moving to the next.</li>
  <li>Continue down the list, moving on only after completing each task.</li>
  <li>At the end of the day, move any unfinished tasks to the next day’s list.</li>
  <li>Repeat daily.</li>
</ol>

<h2 id="why-it-works">Why it works:</h2>

<ul>
  <li>It allows you to focus on what matters most and ignore everything else</li>
  <li>It keeps your to-do list manageable (no more than six tasks)</li>
  <li>It gives you something to look forward to, because you know exactly what to
work on the next day</li>
  <li>It builds daily momentum through clarity and discipline</li>
</ul>

<h2 id="how-to-use-it">How to use it</h2>

<p>Until recently, my mistake in using this method was failing to clearly define my
goals and making sure they were achievable. This led to ambiguous tasks that
never seemed to end, and a list of tasks that kept growing. The solution was to
start making SMART goals.</p>

<p>SMART is a framework for setting clear, actionable goals. Each goal should
be:</p>

<ul>
  <li><strong>Specific</strong>: Clearly defined, focused, and unambiguous</li>
  <li><strong>Measurable</strong>: Quantifiable so progress can be tracked</li>
  <li><strong>Achievable</strong>: Realistic and attainable given resources and constraints</li>
  <li><strong>Relevant</strong>: Aligned with broader objectives and meaningful to you</li>
  <li><strong>Time-bound</strong>: Has a clear deadline or time frame</li>
</ul>

<p>Another surprisingly effective habit is visualizing myself working on the next
day’s tasks. For whatever reason, going through the tasks in my head during the
course of the evening helps me look forward to starting my work the following
day.</p>

<p>Lastly, while it’s all well and good that you use this method to improve your
own productivity, it’s even better to involve your team. If you’re a team lead,
manager, or above, it’s a simple matter to <a href="https://slack.com/help/articles/208423427-Set-a-reminder">add a Slack
reminder</a> to a channel
to remind the team to set their goals for the next day. Why should you be the
only one benefiting from this productivity boost?</p>

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

<p>When Ivy Lee suggested this method to Charles M. Schwab, he only asked that
Schwab send him a check based on the value he thought it brought to his
organization. “After three months, Schwab was so delighted with the progress his
company had made that he called Lee into his office and wrote him a check for
$25,000.” That’s the equivalent of more than $500,000 in 2025. If Charles M.
Schwab found that much value in such a simple change, doesn’t it make sense that
you would too?</p>]]></content><author><name></name></author><category term="productivity" /><summary type="html"><![CDATA[Discover the 100-year-old Ivy Lee Method—a simple daily routine of six tasks that can sharpen focus, build momentum, and boost productivity.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://samuelmullen.com/assets/images/ivy-lee-method/ivy-lee-tasks.png" /><media:content medium="image" url="https://samuelmullen.com/assets/images/ivy-lee-method/ivy-lee-tasks.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">From Stress Test to Skills Test: A Smarter Approach to Technical Interviews</title><link href="https://samuelmullen.com/articles/from-stress-test-to-skills-test" rel="alternate" type="text/html" title="From Stress Test to Skills Test: A Smarter Approach to Technical Interviews" /><published>2025-08-14T05:00:00+00:00</published><updated>2025-08-14T05:00:00+00:00</updated><id>https://samuelmullen.com/articles/from-stress-test-to-skills-test</id><content type="html" xml:base="https://samuelmullen.com/articles/from-stress-test-to-skills-test"><![CDATA[<p><img src="https://samuelmullen.com/assets/images/stress-test-to-skills-test/stress_v_skills.png" class="img-thumbnail img-right" /></p>

<p>20 minutes. That’s how long I sat staring at a blank page while the interviewer
waited, patiently observing. 20 minutes of considering the different
possibilities; 20 minutes of talking myself down from panic; 20 minutes of
painful inactivity. The longer I took the more I worried about what my
interviewer was thinking. I finally gave up trying to figure out the perfect
solution and just started coding something. The mental blocks shattered, and
everything began to fall into place.</p>

<p>20 minutes. That’s how long I sat patiently waiting for her to start solving the
challenge. 20 minutes of wondering if I needed to give her a hint; 20 minutes
of watching her try to calm down; 20 minutes of me trying to stay engaged. The
longer she took, the more I worried this was a waste of both of our time. She
finally gave up, and in tears stopped the interview.</p>

<p>These two moments — separated by perspective, but still similar —
underscored for me that the problem wasn’t just with the candidates. It was with
the interview process itself.</p>

<p>There had to be a better way.</p>

<h2 id="the-purpose-of-the-coding-interview">The Purpose of the Coding Interview</h2>

<p>Coding assessments have become notorious in our industry for presenting problems
you would never face in everyday work, conducted by interviewers more focused on
intellectual sparring than genuine evaluation, and take-home challenges you
spend your weekend working on only to receive a curt followup email thanking you
for your time, but they’ve already filled the position. They are intense and
stressful, they vary from company to company, and they don’t seem to achieve
their stated goal.</p>

<p>Even so, they aren’t without merit.</p>

<p>There are two reasons for the coding interview: to filter out the unqualified,
and to see how candidates think and work. The question is, “Do traditional
technical interviews address these two reasons?” I argue that they do not.</p>

<h2 id="where-the-traditional-coding-interview-falls-short">Where the Traditional Coding Interview Falls Short</h2>

<p>First, consider the reason to filter out the unqualified. Yes, you want to
eliminate candidates who lack the competency for the role, but you don’t want to
eliminate those who are competent, but who don’t perform well under
high-pressure and time-constrained situations.</p>

<p>Consider these findings from Microsoft:</p>

<blockquote>
  <p>The impact on performance was drastic: nearly twice as many participants
failed to solve the problem correctly, and the median correctness score was
cut more than half, when simply being watched by an interviewer.</p>

  <p>—<a href="https://par.nsf.gov/servlets/purl/10196170">Does Stress Impact Technical Interview Performance?</a></p>
</blockquote>

<p>This means traditional interviews can systematically misrepresent candidate
ability. The results for women were even worse:</p>

<blockquote>
  <p>In the public setting, no women (n = 5) successfully solved their task;
however, in the private setting, all women (n = 4) successfully solved their
task—even providing the most optimal solution in two cases.</p>

  <p>—<a href="https://par.nsf.gov/servlets/purl/10196170">Does Stress Impact Technical Interview Performance?</a></p>
</blockquote>

<p>Second, I remain unconvinced that testing candidates’ abilities to solve
<a href="https://leetcode.com">LeetCode</a> challenges, (easy or hard,) shows anything
beyond a person’s ability to memorize challenges and handle high-stress,
test-taking scenarios. Homegrown challenges are often equally unfruitful. When
hiring, I’m looking for a candidate’s ability to assess trade-offs, that they
understand best practices, that they produce quality code, and have a good
<a href="https://www.goodreads.com/book/show/39996759-a-philosophy-of-software-design">philosophy of software design</a>. Traditional technical interviews don’t address these concerns.</p>

<h2 id="a-turning-point">A Turning Point</h2>

<p>The turning point for me came after reading an article about replacing the
technical challenge with code reviews. In this scenario, the interview
transforms from a challenge to a conversation, wherein the candidate is required
to evaluate the code, explain what it does, and provide feedback about how it
might be improved. This style is effective because it reduces the anxiety of the
challenge, reveals problem-solving approaches in a realistic context, and
mirrors the kinds of technical conversations they would have on the job.</p>

<p>After using this on a handful of interviews, however, I found it fell short in
two areas: 1) reading code isn’t the same as writing code; 2) you don’t get to
see how the candidate works. This led me to come up with, what I believe is a
better solution.</p>

<h2 id="a-better-way">A Better Way</h2>

<p>I finally settled on the following solution:</p>

<ul>
  <li>Provide at least one file and associated tests from existing production code
(these are run from the interviewing environment, not in production)</li>
  <li>Modify the code and tests to introduce bugs or inefficiencies as desired and
remove features you want the candidate to add</li>
  <li>Provide a prompt to fix any failing tests, add a new function/method to do
<em>x</em>, and perform any refactorings on the file or tests that make sense.</li>
</ul>

<p>An example prompt might look like the following:</p>

<blockquote>
  <p>The <code class="language-plaintext highlighter-rouge">/ui/version</code> endpoint, represented in the <code class="language-plaintext highlighter-rouge">app.js</code> file, is used by the
AcmeApp to ensure the version used by the backend matches the version running
on the client’s machine. Each time the UI is deployed, it uses the API to set
the version in the database.</p>

  <p>Because this endpoint has been around since the beginning of AcmeApp, it has
gone through several iterations. The current version is a bit of a mess, and
we would like to refactor it.</p>

  <p>It has three endpoints:</p>

  <ul>
    <li><code class="language-plaintext highlighter-rouge">GET /ui/version</code> - returns the current version</li>
    <li><code class="language-plaintext highlighter-rouge">POST /ui/version</code> - sets the current version</li>
    <li><code class="language-plaintext highlighter-rouge">DELETE /ui/version</code> - deletes the current version</li>
  </ul>

  <p>Things to note:</p>

  <p>There should only ever be one version in the database.</p>

  <p>What we need you to do:</p>

  <ul>
    <li>Refactor the code to make it more readable, maintainable, and modern.</li>
    <li>Add tests to ensure the code works as expected</li>
    <li>Some initial tests have been written, but more may be needed</li>
    <li>Fix any bugs you find</li>
    <li>Identify security issues, if any</li>
  </ul>

  <p>Files</p>

  <ul>
    <li><code class="language-plaintext highlighter-rouge">main.js</code> - the test file</li>
    <li><code class="language-plaintext highlighter-rouge">app.js</code> - the actual app file</li>
  </ul>

  <p>— Don’t modify these files —</p>

  <ul>
    <li><code class="language-plaintext highlighter-rouge">support/semver.js</code> - simplified version of the semver library</li>
    <li><code class="language-plaintext highlighter-rouge">support/db.js</code> - a fake database.</li>
    <li><code class="language-plaintext highlighter-rouge">support/http.js</code> - a dumbed down HTTP mocking library for the response object</li>
  </ul>
</blockquote>

<h3 id="why-its-better">Why it’s Better</h3>

<p>What follows are just some of the reasons why performing technical interviews
this way is better:</p>

<h4 id="see-how-candidates-work-in-a-realistic-setting">See how candidates work in a realistic setting</h4>

<p>One of the reasons this is a better solution to the technical interview is that
it better reflects a day-in-the-life of what candidates will do: reading code,
fixing bugs, adding features, writing tests, and reading product requirements.
By using this method, you get insight into how the candidate works and at what
level.</p>

<ul>
  <li>Do they use tests to validate their code?
    <ul>
      <li>Do they even run the tests? (Don’t get me started)</li>
    </ul>
  </li>
  <li>What’s important to them when refactoring?
    <ul>
      <li>An inexperienced engineer might waste time reformatting a file to fit
their preferences while more experienced engineers will focus on
efficiencies, good naming, and design.</li>
    </ul>
  </li>
  <li>Do they follow instructions or make assumptions?
    <ul>
      <li>There have been many instances where candidates change files they were
explicitly told not to modify.</li>
    </ul>
  </li>
  <li>Can they do the work, and at what level?</li>
</ul>

<h4 id="avoid-the-blank-page-problem">Avoid the blank-page problem</h4>

<p>One of the biggest problems with traditional coding challenges is facing the
blank editor. How should I start this? How should I organize it? What’s the
right solution? You can imagine a dozen different ways to solve it, but you’re
not sure which way is optimal. When you are given an existing page of code, on
the other hand, you’re able to focus on what the code is currently doing, and
sidestep some of the anxiety brought on by facing the <a href="https://en.wikipedia.org/wiki/The_Paradox_of_Choice">Paradox of Choice</a>.</p>

<h4 id="encourages-a-two-way-technical-discussion">Encourages a two-way technical discussion</h4>

<p>The whole point of the interview process is to determine if a candidate is a
good fit for your team: Are they qualified? Are they a good culture fit? Are
they better than the rest of the candidates? Portions of these questions will be
answered in other interviews, but additional information can be provided in the
technical interview as well. As the candidate “solves” the challenge, it’s
important to keep dialogue open to understand why they’re making the choices
they are:</p>

<ul>
  <li>Why was it important to do x or y?</li>
  <li>Did you consider x?</li>
  <li>Why did you break out that block into a new function?</li>
  <li>Why did you make that a private method?</li>
  <li>Is that change optimal? Why? Is it important that it is?</li>
</ul>

<p>These are just a few of the many types of questions you can ask during the
process, the purpose of which is to better understand why the make the choices
they do, the trade-offs they consider, and if this is someone you or your team
would want to work with.</p>

<h4 id="adjusts-for-your-company-and-your-project">Adjusts for your company and your project</h4>

<p>As stated, this type of technical interview should use code from an existing
production code base. This allows your interviews to be specific to your company
and project. By doing it this way it allows you to see if candidates have the
background and experience to perform the role to which they are applying. For
example, a database company might use a challenge that only those who have
worked with database would be able to answer. Likewise, companies focused on AI,
web applications, search, or anything else could tailor the challenge for their
specific industry, and not settle for a cookie-cutter coding challenge.</p>

<h2 id="an-imperfect-solution">An Imperfect Solution</h2>

<p>There isn’t a perfect technical interview. Candidates’ anxiety will still run
higher than in Q&amp;A style interviews, interviewers will still occasionally be
detached or condescending, and the wrong candidate will still get through when
the better one gets left behind. We can’t attain perfect, but we can and should improve our process so we can get closer.</p>

<p>Traditional interviews fall short in helping candidates overcome anxiety and
accurately measuring competence and cultural fit. Attempts have been made to
improve technical interviews with take-home challenges, code review interviews,
and Bug Squash interviews. Each iteration has made improvements, but have also
introduced other problems.</p>

<p>The solution presented in this article seeks to address the issues in the
traditional coding interview as well as the problems in other types as well. It
affords the candidate the opportunity to show how they would work against
a real-world problem, provides greater opportunities for interviewers to ask
questions and get a feel for culture fit, it helps alleviate anxiety by starting
candidates off with existing code, and it can be adjusted for any company and
project.</p>

<p>It may not be perfect, but it’s a proven step toward more accurate, fair, and
effective hiring — and worth serious consideration by any team that wants
to improve the quality of its hires.</p>]]></content><author><name></name></author><category term="hiring" /><category term="interviewing" /><category term="employment" /><summary type="html"><![CDATA[Stop testing nerves. Start testing skills. Here’s a coding interview that’s fair, real, and actually worth everyone’s time.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://samuelmullen.com/assets/images/stress-test-to-skills-test/stress_v_skills.png" /><media:content medium="image" url="https://samuelmullen.com/assets/images/stress-test-to-skills-test/stress_v_skills.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Mind the Gap</title><link href="https://samuelmullen.com/articles/mind-the-gap" rel="alternate" type="text/html" title="Mind the Gap" /><published>2025-08-04T05:00:00+00:00</published><updated>2025-08-04T05:00:00+00:00</updated><id>https://samuelmullen.com/articles/mind-the-gap</id><content type="html" xml:base="https://samuelmullen.com/articles/mind-the-gap"><![CDATA[<p><img src="https://samuelmullen.com/assets/images/mind-the-gap/mind_the_gap.jpg" class="img-thumbnail img-right" /></p>

<p>It should come as no surprise that managers talk about their teams with one
another: who’s high-maintenance and who’s easy, who’s performing well and who’s
not, and who’s adding positive energy to the team and who’s a cancer. I say it
should come as no surprise, because team members have similar conversations.
What might surprise you are the conversations we have about people we know need
to be promoted. Often, these conversations begin with a manager discussing their
own team members, but plenty of conversations are started by managers who
recognize the value of individuals on other teams. We want to see effort and
value rewarded.</p>

<p>If you’ve struggled to advance from a junior position to a senior one, or from
individual contributor to management, the question you might be asking is, “How
can I become that person management wants to promote?”</p>

<p>In short: mind the gap.</p>

<p>A gap, as you already know, is the space between two things. In the case of the
London Underground – where the term “<a href="https://en.wikipedia.org/wiki/Mind_the_gap">Mind the
Gap</a>” originated – it’s the space
between the landing and the train car. In business it could be the gap between
your experience and what’s required for the next level, the amount of work left
undone, opportunities that are missed, or the knowledge and experience that is
lacking in the organization. Or, to put it another way, it’s what you’re lacking
(internal gaps) or what the organization is lacking (external gaps).</p>

<h2 id="internal-gaps">Internal Gaps</h2>

<p><img src="https://samuelmullen.com/assets/images/mind-the-gap/know_thyself.jpg" class="img-thumbnail img-right" /></p>

<p>Inscribed upon the Temple of Apollo in the ancient Greek precinct of Delphi, was
the phrase, “Know thyself.” It was a phrase instructing the reader to understand
what they were capable of, where they fell short, what their potential was, and
above all, to be honest with themselves about it all. After all, no one is
easier to deceive than oneself.</p>

<p>To that end, before you can advance to the next level, you need to understand
where you are. The easiest way to do that is to look at the job ladder for your
position: are you meeting all the requirements for your current position? What
are you lacking for the next position up? What comes naturally, and what
doesn’t? There are going to be areas you don’t want to focus on, because they
don’t come easily, but those are likely the areas you need to work on most.</p>

<p>If your company doesn’t have a career ladder designed for your position, you
have a couple options. The first is to look outside your company to see what
other companies have set up. The second is to look at people within your company
who have the position you’re aiming for and compare yourself against them. You
might even schedule a conversation with them about how they advanced into the
position.</p>

<p>Although I’m putting this last in the section, it’s the most important: talk to
your boss about their perspective on where you fall short. They should know
better than anyone what they’re looking for and what’s expected, and they should
also know where you are in relation to that next step. Be warned, however, they
may point out that you still have work to do to fulfill your current position.
Regardless of what they say—and potentially how they say it—listen
to them and consider their feedback. They’re the one who will be recommending
you for promotion, and ignoring their feedback will only set you up for failure.</p>

<h2 id="external-gaps">External Gaps</h2>

<p>If dealing with your internal gaps is about knowing yourself, then external
gaps are all about knowing your team, your boss, and your organization. It’s
about recognizing what they’re missing or are unable to get to, and stepping up
to support them. Addressing your internal gaps is like providing an argument
against passing you over due to a lack of competency. Addressing external gaps,
on the other hand, argues for recognizing the position you’re already
fulfilling.</p>

<p>If, as they say, “all politics is local,” then it makes sense to first focus on
your local environment, i.e. your current team. The first thing to look for is
what’s going undone or what would help improve productivity. Other things to
consider are becoming the “go-to person” for information about your project, or
becoming the most helpful person on the team. How you go about that will be
unique to your team, but one thing is for certain: you’ll need to outwork
everyone else.</p>

<aside class="panel panel-default pull-right col-md-4">
<h3>In practice</h3>
<p>There was an engineer on the first team I managed who regularly asked me if
there was anything I needed help with, for extra work, or he could run meetings
while I was on PTO. After a while, he didn't need to ask, because he was the
first person I thought of when I needed help or needed to delegate work. He
ended up promoted and managing the team when I moved on.</p>
</aside>

<p>Expanding your perspective from looking at the gaps in your team, it’s time to
look at the gaps your boss is either overlooking or unable to address. The
easiest thing you can do here is to ask if there’s anything you can take of his
or her plate—It goes without saying that you should do this only when all
your other responsibilities are taken care of. Beyond that, listen for what they
complain about in meetings, or areas of improvement they often “wish” for.
Consider also what you would do when faced with the same decisions and
be—respectfully—sound counsel for your manager. Having said all of
this, remember, your goal shouldn’t be to replace your manager prematurely, but
by being the obvious choice to replace them when they are promoted or leave.</p>

<p>The last area to look at is your organization. This can be your department,
region, or even the entire company. Regardless of the size, all organizations
have gaps. They tend to be more easily seen in small companiies, but larger
companies have gaps too—often around their ability to maneuver.</p>

<h2 id="miscellaneous">Miscellaneous</h2>

<p>Not everything fits neatly in the categories of internal and external gaps, but
they warrant brief discussions.</p>

<h3 id="goals">Goals</h3>

<p>Before angling for that next promotion, you need to know what it is you’re
aiming for, because that will dictate how you fill in the gaps. If you want
promoted on the IC (Individual Contributor) path (e.g. from Software Engineer
to Senior Software Engineer) then the two things you need to do are performing
the work of that next role and consistently outworking your peers. If you
are on the management track, however, doing more IC work may not cut it. In
those cases, your leaders may be looking for your ability to delegate and manage
projects, not just do all the work for the project.</p>

<h3 id="capacity">Capacity</h3>

<p>Unfortunately, you could be doing all the right things: working harder, helping
your boss, being the go-to person on your team, and it still may not be enough.
There are any number of reasons for this, but it usually comes down to capacity.
If there are no openings or if there isn’t the budget for the promotion, there’s
not much you can do. But, there is <em>something</em> you can do.</p>

<p>Hiring freezes and resource constraints are temporary. If you are serious about
moving up to that next rung of the career ladder, use this time to prepare
yourself for that next position; it will open up eventually. Focus on your
growth and do what you can to make sure you’re the obvious choice when
constraints ease up.</p>

<h3 id="reality">Reality</h3>

<p>Here’s the ice-cold bucket of reality: you are not owed or guaranteed a
promotion, a raise, or even kudos. You may be doing the work of five people and
most of your boss’ job, but your company doesn’t owe you anything other than a
paycheck and not violate certain laws. It sucks, and maybe it seems unfair, but
that’s the way it is. If you’re in this sort of position, you should talk to
your manager to find out why.</p>

<p>“But I’m the victim or racism, sexism, ageism, homophobia, religiophobia, or
some other -ism or -phobia.” You might be. It happens, and you would be
surprised at how frequently it happens. It could also be that someone doesn’t
like you in particular. This also happens. Everyone has obstacles they’re trying
to overcome. Everyone. Some people have larger obstacles than others. Some
people are smarter, prettier, or better socially than others. It’s reality. The
way to get around that is to “<a href="https://www.goodreads.com/book/show/13525945-so-good-they-can-t-ignore-you">be so good they can’t ignore
you</a>.”</p>

<p>I’ve mentioned “career ladder” a couple times in this article, but the reality
is that oftentimes the “ladder” is more like a jungle gym, where you move
horizontally or downward in order to move up to the position you really want. In
both cases above, the way forward may be to move horizontally or (what feels
like) backwards. Leaving your role for another, even if it’s inside your
company, can feel like a setback, but is it worse than waiting on something
that’s out of your control or actively working against you?</p>

<h2 id="final-thoughts">Final Thoughts</h2>

<p>“Mind the gap.” It’s not only good advice for stepping off a subway car, it’s
also good for your career. The gaps you face can be internal: skills,
experience, knowledge, or even your network. Gaps can also be found externally:
the work your boss isn’t able to get to or isn’t aware of, opportunities on your
team, and also areas in your organization which go unnoticed. Sometimes gaps are
too big to overcome because of limited resources, discrimination, or something
else entirely. Some gaps can only be overcome by going around or even backwards.</p>

<p>Promotions rarely go to the person who simply wants the job—they go to the
person who’s ready to fill it. Look around, where are the gaps? Fill them.
Quietly. Consistently. Continuously. When the time comes, no one will question
whether you’re ready; they’ll assume the job is yours already.</p>]]></content><author><name></name></author><category term="employment" /><category term="work" /><category term="career" /><summary type="html"><![CDATA[Promotions go to those who do the job before they get the title. Discover how to mind the gap and position yourself for the next step in your career.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://samuelmullen.com/assets/images/mind-the-gap/mind_the_gap.jpg" /><media:content medium="image" url="https://samuelmullen.com/assets/images/mind-the-gap/mind_the_gap.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">I Got Laid Off. Now What?</title><link href="https://samuelmullen.com/articles/i-got-laid-off-now-what" rel="alternate" type="text/html" title="I Got Laid Off. Now What?" /><published>2025-07-25T05:00:00+00:00</published><updated>2025-07-25T05:00:00+00:00</updated><id>https://samuelmullen.com/articles/i-got-laid-off-now-what</id><content type="html" xml:base="https://samuelmullen.com/articles/i-got-laid-off-now-what"><![CDATA[<h1 id="what-happened">What happened?</h1>

<p><img src="//samuelmullen.com/assets/images/avatar-lg-2025.png" class="img-thumbnail img-responsive img-right" alt="Avatar" title="Avatar" width="600" /></p>

<p>“Huh. That’s weird.” That was my first thought when I received a calendar invite
from our CPO (Chief Product Officer)/head of engineering (which is also weird).
It shouldn’t have been weird since there was only a single layer (my director)
between him and myself, but I’d only met with him once since he started eight
months ago, so yeah, it was weird.</p>

<p>Within moments of joining the meeting the CEO and head of HR also joined. I
immediately knew what was happening and said, “Oh. I’m getting laid off.” And
that’s what was happening. After the CPO read his script he bounced and the CEO
and HR guy explained the <em>“why”</em> and <em>“what for”</em>, and told me I would be able
to work for another month and then receive a decent severance package.</p>

<p>It’s better than most people get.</p>

<h1 id="what-now">What now?</h1>

<p>No one tells you this, but when you lose your job you immediately get a new job;
it just takes a while to figure that out. My new job is finding a new job. It’s
obvious from the outside, but when you’re blindsided by getting tossed aside it
can take a while for reality to settle in; even when you know what you’re
supposed to do from the very start.</p>

<p>My days now consist of doing everything I can to find the next opportunity:
applying for jobs, writing articles to get noticed, researching and
experimenting with new technology, and [shudder] network. There’s, of course, no
way I can spend 40 hours a week doing all of that, so I intend to practice bass
guitar, spend time woodworking, and help out more around the house.</p>

<h1 id="what-the-hell">What the hell?</h1>

<p>As of this writing, it’s been a month and I think I’m at a good place now. I’m
less anxious about the future in spite of only having one interview in that
time, and in spite of hearing more and more stories about
<a href="https://layoffs.fyi">layoffs</a>. I won’t bore you with my reasoning, but I think
this happened at the right time and I think something better is around the
corner. I’m hopeful for the future.</p>

<p>Lest anyone think I’ve risen above my baser emotions like anger and resentment,
I haven’t. I still have moments—usually while working out—where
those emotions rise to the surface. It’s just part of the process, which I’m
trying to embrace. Do I have moments where I want to lash out and leave a
scathing <a href="https://www.glassdoor.com">GlassDoor</a> review or whine about it on
social media? Of course, but I’m not going to.</p>

<blockquote>
  <p>2 Consider it all joy, my brethren, when you encounter various trials, 3
knowing that the testing of your faith produces endurance. 4 And let endurance
have its perfect result, so that you may be perfect and complete, lacking in
nothing.</p>

  <p>—<a href="https://nasb.literalword.com/?q=james+1%3A2-4">James 1:2-4</a></p>
</blockquote>

<p>There’s also the matter of embarrassment. It’s embarrassing to lose your job.
It’s embarrassing to have to tell your wife and kids you were laid off. It’s
embarrassing to add that “green circle of shame” to your <a href="https://linkedin.com/in/samuelmullen/">LinkedIn
profile</a>. But so what? I know what I
accomplished while at ActiveProspect, I know I have the respect of my peers and
the teams I managed while working there. Everything else is ego and emotional
noise. The truth is, sometimes “bad” things happen to you regardless of the
efforts you put in.</p>

<blockquote>
  <p>When we are guests at a dinner party, we content ourselves with the food on
offer; if anyone were to tell the host to put out fish or cake, he would seem
rude. In real life, however, we ask the gods for what they do not give, and
this though they have provided us with plenty.
– Epictetus, Fragments, 17</p>
</blockquote>

<p>Or as Job put it:</p>

<blockquote>
  <p>Shall we indeed accept good from God and not accept adversity?</p>

  <p>—<a href="https://nasb.literalword.com/?q=job%202:10">Job 2:10</a></p>
</blockquote>

<p>I’m genuinely grateful for the six years I had at ActiveProspect. I started as a
Senior Elixir Engineer and ended as a Senior Engineering Manager. I hired a lot
of great talent (most of whom are still there), made some great friends, learned
a lot about scaling, startups, and management, and I have no regrets about the
work I did there. ActiveProspect was a blessing to me and my family for these
past six years, and I’m grateful for every moment of it; even the layoff.</p>

<h1 id="whats-next">What’s next?</h1>

<p>I have no idea where I’ll end up next. Ideally I’ll end up managing another
software development team for a small to medium company and I’ve applied for a
number of roles along those lines, but I’m open to doing something completely
different too. I have some ideas of where I’d like to take my career, but that’s
three to five years out. In the meantime I’ll be exploring AI quite a bit in
conjunction with working on <a href="https://makerplans.io">Makerplans</a>.</p>

<p>What I do know is this is not a setback, and it’s only temporary.</p>]]></content><author><name></name></author><category term="personal" /><category term="employment" /><summary type="html"><![CDATA[I got laid off. Then came the questions: what happened, what now, and what’s next? This is a personal reflection on losing a job, dealing with the aftermath, and finding a way forward.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://samuelmullen.com/assets/images/avatar-lg-2025.png" /><media:content medium="image" url="https://samuelmullen.com/assets/images/avatar-lg-2025.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Making the Most of Your 1-1s</title><link href="https://samuelmullen.com/articles/making-the-most-of-your-1-1s" rel="alternate" type="text/html" title="Making the Most of Your 1-1s" /><published>2023-03-16T05:00:00+00:00</published><updated>2023-03-16T05:00:00+00:00</updated><id>https://samuelmullen.com/articles/making-the-most-of-your-1-1s</id><content type="html" xml:base="https://samuelmullen.com/articles/making-the-most-of-your-1-1s"><![CDATA[<p>I didn’t have 1-1s with my managers for most of my career, mostly because they
weren’t as commonplace as they are now. When I did have them, I didn’t know
what to do or ask and instead let my manager drive the conversation. It wasn’t
until much later that I discovered their value. Now that I’m in management, I
see things from the other side, and I realize how many opportunities I
squandered by not taking control of these meetings.</p>

<p>The 1-1 isn’t intended to be a time to update your manager about what you’re
doing, or whether or not you’ll get your tasks done on time. Instead, as Andy
Grove points out in <a href="https://www.goodreads.com/book/show/324750.High_Output_Management">High Output
Management</a>,
it’s about “…mutual teaching and exchange of information. By talking about
specific problems and situations, the supervisor teaches the subordinate his
skills and know-how, and suggest ways to approach things.” The 1-1 is for you
and your
professional development.</p>

<h2 id="the-purpose-of-the-1-1">The Purpose of the 1-1</h2>

<p>In her book, <a href="https://www.goodreads.com/book/show/33369254-the-manager-s-path">The Manager’s
Path</a>, Camille
Fournier identifies the two main purposes of
the 1-1:</p>

<ol>
  <li>“[T]hey create connection between you and your manager.”</li>
  <li>They provide “a regular opportunity for you to speak privately with your
manager about whatever needs discussing.”</li>
</ol>

<p>The second item is obvious and most people might be satisfied accepting that as
the sole purpose of the 1-1. What’s less obvious is the value inherent in item
one. Connecting with your manager is valuable for the following reasons: 1) you
feel less like an island on your team; 2) you get reassurance that you belong;
and 3) you learn where you stand with your boss and within your team.</p>

<p>If you’ve ever gone for an extended period of time without connecting with your
manager you know how isolating it can be, and it’s this understanding that
argues that 1-1s are for <em>you</em>, not your manager.</p>

<h2 id="1-1s-are-for-you-not-your-manager">1-1s Are For You, Not Your Manager</h2>

<p>Every manager has his or her own way of running 1-1s, and they’re different for
each of their reports, and change over time. In spite of that, Andy Grove argues
the 1-1 “should be regarded as the reports’s meeting, with its agenda and tone
set by him.” (<a href="https://www.goodreads.com/book/show/324750.High_Output_Management">High Output
Management</a>)
To that end, Grove continues, the manager “should facilitate the subordinate’s
expression of what’s going on and what’s bothering him.” So even though a
manager may have a particular way of running it, the 1-1 is still for you.</p>

<p>The fact that 1-1s are for you, but facilitated by your manager, doesn’t
absolve you from your role and responsibilities therein. You still need to show
up with questions and items to discuss, be able to provide answers to your
manager’s questions, and have goals for what you want to accomplish in the
meeting.  Your manager will facilitate the 1-1, but they shouldn’t be the main
driver.</p>

<p>However, there will be times when your manager drives and directs the 1-1. It
may be that they have news or information they need to share with you, they need
to have a performance discussion with you, they want to focus on your career
growth, or they have something else they need to talk about. This type of 1-1s
will generally be more infrequent, and you should still come prepared.</p>

<p>Lastly, consider what it would be like if you allowed your manager to always
direct the 1-1. Do you really trust someone else to know what is best for you,
to say what path you should take, or make decisions for you? But every time you
show up to a 1-1 without preparation, it’s doing this very thing. The more
frequently you abdicate your responsibility, the more control you give to
someone else. The choices they make for you will be the ones that are easiest
for them, not the ones that are best for you.</p>

<h2 id="come-prepared">Come Prepared</h2>

<p>By this point we should be agreed that 1-1s are about the team member, that they
should be driving them, and to do so, the team member needs to come prepared. If
that’s so, what do you need to do?</p>

<p><strong>Come with items to discuss</strong></p>

<p>This should be obvious, but make sure you come to your 1-1 with a few things to
discuss with your manager. How many you bring will depend on the nature of the
topics. I advise my teams to add items to their list as things come up through
the week. Here are some potential topics to discuss:</p>

<ul>
  <li><strong>Status update:</strong> Limit this to just a few minutes. Most status updates occur
during standups or as comments in your project management tool, and you waste
valuable time with your manager telling them things they should already know
from other meetings.</li>
  <li><strong>Accomplishments:</strong> Share your accomplishments with your manager: Course
completions, finishing projects, writing blog posts, giving conference talks,
etc. are all things they should know about</li>
  <li><strong>Problems:</strong> Inform your manager about problems (technical, interpersonal,
etc) you run across. If possible, come with solutions, or at least show you’ve
researched how to solve the problem. No manager wants yet another problem
dropped in their lap.</li>
  <li><strong>Ideas:</strong> If you have ideas to make things better for your project, team,
company, or anything else, bring them to the 1-1 for discussion.</li>
  <li><strong>Research topics:</strong> If you’ve been researching something to improve yourself
of the team, a 1-1 is great time to share what you’ve uncovered.</li>
  <li><strong>Team needs:</strong> Let your manager know about the needs of your team. New
technologies, missing skill sets, tools, access, etc. are all things you
should take to them.</li>
  <li><strong>Goal check-in:</strong> Let your manager know about the progress you’ve made on
your goals.</li>
  <li><strong>Ask about your manager’s challenges:</strong> It’s okay to ask about what your
manager needs. It shows that you’re thinking outside of yourself, and are
looking to expand your responsibilities.</li>
  <li><strong>Feedback:</strong> Ask for feedback, but be prepared to hear both positive and
constructive feedback. Furthermore, only ask for it if you’re really looking
to improve.</li>
</ul>

<p><strong>Be prepared to answer questions</strong></p>

<p>Managers are going to ask questions: about you, your work, your aspirations, and
more. You can also expect them to ask probing questions, trying to dig deeper on
certain topics such as key projects or interpersonal relationships. If your
manager is really good, they’ll use questions to help you gain a greater
understanding about the business, your role, and yourself.</p>

<p>Your responsibility is to come prepared to answer these questions. You should be
able to provide thorough answers about your work and career aspirations. There
will also be questions which will blindside you, against which you really
<em>can’t</em> prepare; for those, you just have to roll with the punches and do the
best you can. In most cases, however, you should expect to be able to provide
reasonable answers for all of your manager’s questions.</p>

<p><strong>Show you’ve worked on action items</strong></p>

<p>You should have at least one “action item” you take away from every 1-1;
something you’ve learned or can apply. These action items aren’t always
explicit, and could be something as simple as researching a paper or book your
manager mentioned off-handedly. You should be able to show that you made
progress on that action item the next time you meet with them.</p>

<p><strong>Take notes</strong></p>

<p>This shouldn’t need to be said, but I know it does: take notes. Organize them by
date so your manager can corroborate with them. It won’t seem useful in the
beginning, but eventually your notes will reach a critical mass and you’ll
rely on them more and more to answer questions. Also, if your manager
acts unbecomingly or if a sensitive topic is discussed, you’ll want to have
your notes, especially if you’re the one in the hot seat.</p>

<p>If you don’t have 1-1s with your manager, or have only recently started them,
don’t worry, you’re not alone. In an informal poll I conducted on LinkedIn, over
33% of the respondents said they’ve only started having 1-1s with their manager
in the past two years, and I had a number of people lament there was no option
for zero years. Whether you’ve had 1-1s for years or just started, there’s no
time like the present to start working to make them better. On the other hand,
if your manager doesn’t have regular 1-1s with you, there’s no reason you can’t
schedule one with them.</p>

<p>Now that I’m in management, I have more 1-1s than ever. Not just with my team
and director, but our VP, another engineering manager, the director of product,
and others, and the frequencies varies. Some are to seek alignment, such as my
1-1 with the head of Product, others are for advice and council, while at least
one is just to connect, but they’re all valuable and make me a better manager
and leader.</p>]]></content><author><name></name></author><category term="management" /><category term="1-1s" /><category term="meetings" /><summary type="html"><![CDATA[1-1s are intended for the team member, not the manager. In this article we look at how we come to that conclusion and what we can do to make the most of our 1-1s.]]></summary></entry><entry><title type="html">The Team Refactor Meeting</title><link href="https://samuelmullen.com/articles/the-team-refactor-meeting" rel="alternate" type="text/html" title="The Team Refactor Meeting" /><published>2022-04-11T11:10:30+00:00</published><updated>2022-04-11T11:10:30+00:00</updated><id>https://samuelmullen.com/articles/the-team-refactor-meeting</id><content type="html" xml:base="https://samuelmullen.com/articles/the-team-refactor-meeting"><![CDATA[<p>Few engineers are immune to the siren’s song to refactor a piece of code. The
idea of taking a block of crufty, unreadable, inefficient code and improving it
is too strong of an allure for most of us to resist. But what starts as a
refactor, more often  than not, results in a restructuring of the code. What’s
the difference? As Martin Fowler is often quoted:</p>

<blockquote>
  <p>Refactoring is the process of changing a software system in such a way that it
does not alter the external behavior of the code yet improves its internal
structure.</p>

  <p>– Martin Fowler</p>
</blockquote>

<p>Restructuring differs from refactoring in that it alters the external behavior.
Argument types and arity, function names, classes, and modules can all be
dramatically changed with a restructure. So can the return value.</p>

<p>I’m managing teams now, and aside from code reviews and the occasional one or
two point story, I rarely get a chance to write code, let alone refactor
anything. Nonetheless, I’m still captivated by refactoring; for the chance to
improve processes and code.</p>

<p>I first stumbled on the idea of the team refactor meeting–as I most often
do–after meeting with one of my team. In that meeting he made an offhanded
remark about getting together with the team to consider which areas of the
project needed the most work. Something about the way he said it gave me the
idea for an ongoing meeting to make everything about the way we work better.</p>

<h2 id="what-is-the-team-refactor">What is the Team Refactor?</h2>

<p>Unlike refactoring code, the team refactor is a meeting used to review and
rethink the way your team and project operates. It’s a time to address the pain
points we experience–but often ignore–in our project, processes, and the code
base. It gives us the chance to ask questions such as:</p>

<ul>
  <li>Where are the problem areas in our code base?</li>
  <li>Where are the bottlenecks in the system?</li>
  <li>Which processes are we struggling with the most?</li>
  <li>What can we do to improve our communication?</li>
  <li>What would make the development experience better?</li>
  <li>Which process is the most difficult to understand?</li>
</ul>

<p>It’s tempting to confuse this with a retrospective, but the two are altogether
different. A retrospective focuses on the successes, failures, and lessons drawn
from a sprint or feature release, whereas the team refactor takes a step back to
look at the bigger picture, concentrating more on what can be done to make the
team more effective.</p>

<h2 id="how-does-it-work">How Does it Work?</h2>

<p>The team refactor is a meeting, and should be held no more than once a month,
and no less than once a quarter (we hold ours on the last Friday of every
month). If it’s held weekly or bi-weekly, it’s difficult to see progress and you
risk it devolving into a gripe session. Anything less than once a quarter and
you’ll lose engagement. I recommend basing the frequency of the meeting on the
amount of the team’s technical or organizational debt.</p>

<p>To make certain there are topics to discuss, regularly remind the team about it
leading up to the meeting (such as in daily standups). It keeps the meeting “top
of mind”, giving everyone time to watch for areas to improve. Make sure to keep
a list of your own topics in case things stall out.</p>

<p>You can run the meeting any way you prefer. What I’ve done so far is break it
up into three parts: Review, Propose, and Prioritize.</p>

<h3 id="review">Review</h3>

<p>During the “review” section of the meeting, your job is to remind the team what
was covered in the last team refactor, highlighting which items the team
completed. If no progress was made, own up to it; it’s better for morale if you
don’t hide from it.</p>

<p>This part usually takes less than a third of the meeting.</p>

<p><strong>Note:</strong> The “review” only works if you keep notes from one session to another.</p>

<h3 id="propose">Propose</h3>

<p>The bulk of the meeting should be dedicated to sharing and talking about new
ideas and topics. During this time, each team member should have something
they’d like to see improved, with the team lead or manager offering ideas after
everyone else or if no one is willing to open up.</p>

<h3 id="prioritize">Prioritize</h3>

<p>Use the last segment of the meeting to prioritize what the team plans to do.
Which of the ideas discussed this time and in prior meetings should be
tackled, and in what order? This part of the meeting should take the least
amount of time.</p>

<h2 id="what-now">What Now?</h2>

<p>Once the meeting concludes, it’s imperative you create stories to address the
problems the team brought up. There’s no point in holding a team refactor unless
the team can execute on those items. If the team isn’t allowed to
act, you’ll deliver a gut punch to their morale. Don’t do that. Instead, make
sure to schedule one or two items for work before the next team refactor. The
stories don’t have to be completed, but they should at least be started.</p>

<h2 id="so-what">So What?</h2>

<p>Refactoring attracts developers, because it’s an opportunity to improve a
piece of code; a way of proving we can solve the problem better. It’s the same
thing for the team refactor. It’s coming together as a group, recognizing we can
improve every area of our project, and then setting out to do it.</p>

<p>For the engineers, this means making the work environment–be it the system,
processes, or codebase–more enjoyable to work in. They get to focus on solving
problems instead of fighting the system or struggling to untangle spaghetti.</p>

<p>For the manager or the lead, it means seeing the team more engaged by proving
that things <em>can</em> get better and <em>are</em> getting better. It also shows that you
hear and understand them.</p>

<p>I’ve run team refactors on two different teams for a year, and I can honestly
say it works. There may be griping in the first meeting or two about “yet
another meeting,” but it settles down once everyone sees things improve.  Once
they see they’re the ones responsible for that change–that they’re the ones
driving it–they only want more. That’s a good thing.</p>]]></content><author><name></name></author><category term="management" /><category term="teams" /><category term="meetings" /><summary type="html"><![CDATA[With everything we have to do, is another meeting necessary? It is if you want to engage your engineering staff and taking ownership over the product they work on.]]></summary></entry></feed>