<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Christian Alexander</title>
    <description>The latest articles on DEV Community by Christian Alexander (@christianalexander).</description>
    <link>https://dev.to/christianalexander</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F6761%2Fceaf625a-39de-4a60-992e-a746b1d9ac3c.jpg</url>
      <title>DEV Community: Christian Alexander</title>
      <link>https://dev.to/christianalexander</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/christianalexander"/>
    <language>en</language>
    <item>
      <title>Async Elixir Telemetry</title>
      <dc:creator>Christian Alexander</dc:creator>
      <pubDate>Thu, 22 Feb 2024 13:17:54 +0000</pubDate>
      <link>https://dev.to/christianalexander/async-elixir-telemetry-2llo</link>
      <guid>https://dev.to/christianalexander/async-elixir-telemetry-2llo</guid>
      <description>&lt;p&gt;&lt;em&gt;Learn how to use Telemetry and GenServer in Elixir to wire up analytics without sacrificing app responsiveness.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;One of the most fantastic abstractions used in many Elixir applications is provided by &lt;a href="https://hexdocs.pm/telemetry/readme.html"&gt;the Telemetry library&lt;/a&gt;. It allows libraries to emit arbitrary events that other code can subscribe to. This is similar to a Node.js &lt;a href="https://nodejs.org/en/learn/asynchronous-work/the-nodejs-event-emitter"&gt;event emitter&lt;/a&gt;, but make it global.&lt;/p&gt;

&lt;p&gt;While this doesn't seem very important at first glance, the power of Telemetry comes from its vast adoption. Packages such as &lt;a href="https://hexdocs.pm/phoenix/telemetry.html"&gt;Phoenix&lt;/a&gt; (the popular web framework), &lt;a href="https://hexdocs.pm/oban/Oban.Telemetry.html"&gt;Oban&lt;/a&gt; (the popular background job framework), and &lt;a href="https://hexdocs.pm/finch/Finch.Telemetry.html"&gt;Finch&lt;/a&gt; (the HTTP request library used by &lt;a href="https://hexdocs.pm/req/Req.html"&gt;Req&lt;/a&gt;) all emit events through Telemetry.&lt;/p&gt;

&lt;p&gt;Typically, Telemetry is used to publish metrics through systems like Prometheus, StatsD, and CloudWatch. These solutions often rely on the &lt;a href="https://hex.pm/packages/telemetry_metrics"&gt;telemetry_metrics&lt;/a&gt; library and consume numeric values included in many telemetry events.&lt;/p&gt;

&lt;p&gt;Telemetry also tends to be used for request logging, since every Phoenix request emits a series of events containing the requested path, method, IP, and response status code. It's a really convenient way to plug in logging services without altering router and controller code.&lt;/p&gt;

&lt;p&gt;The most important limitation to consider when using telemetry can be found in the readme:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;handle_event&lt;/code&gt; callback of each handler is invoked synchronously on each &lt;code&gt;telemetry:execute&lt;/code&gt; call. Therefore, it is extremely important to avoid blocking operations. If you need to perform any action that is not immediate, consider offloading the work to a separate process (or a pool of processes) by sending a message.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This brings me to the topic of today's post, but first I want to set some context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Intent: Capture Product Analytics
&lt;/h2&gt;

&lt;p&gt;Recently I've been working on a lightweight sprint point estimation tool using Phoenix LiveView. This tool is called &lt;a href="https://sprinty.dev"&gt;Sprinty Points&lt;/a&gt;, and I think most agile teams will like it.&lt;/p&gt;

&lt;p&gt;Sprinty Points doesn't yet have a database. Every user is anonymous and session state lives in &lt;a href="https://hexdocs.pm/elixir/1.16.1/GenServer.html"&gt;GenServers&lt;/a&gt;. This has allowed me to build a very performant prototype really quickly without worrying about schema evolution.&lt;/p&gt;

&lt;p&gt;Unfortunately, this architecture makes it hard for me to keep track of user metrics and the growth of the application. Without a &lt;code&gt;users&lt;/code&gt; table, it's hard for me to know how many users I have. Without an &lt;code&gt;events&lt;/code&gt; or &lt;code&gt;sessions&lt;/code&gt; table, it's hard for me to get an estimate on usage.&lt;/p&gt;

&lt;p&gt;To get some very basic metrics, I looked around and found an analytics tool with a generous free tier. It's called &lt;a href="https://posthog.com/"&gt;PostHog&lt;/a&gt; (not sponsored, just a fan). They have a &lt;a href="https://posthog.com/docs/api/post-only-endpoints"&gt;very simple HTTP API&lt;/a&gt; through which I can post user registrations and custom events. These events can be turned into dashboards.&lt;/p&gt;

&lt;p&gt;Since I wasn't very attached to any specific analytics application, I decided to not directly call their API from my handler. Instead, I added some custom telemetry events to a few key controllers and wired up a custom Telemetry handler to send the events out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sample Code, Synchronous Approach
&lt;/h3&gt;

&lt;p&gt;The first approach I took involved three parts: create a simple PostHog client module, wire up a simple Telemetry handler, and start emitting events from Phoenix controllers.&lt;/p&gt;

&lt;h4&gt;
  
  
  PostHog Client Module
&lt;/h4&gt;

&lt;p&gt;The client module is a basic wrapper around their capture endpoint, using Req for HTTP and an environment variable for the API key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;Posthog&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;require&lt;/span&gt; &lt;span class="no"&gt;Logger&lt;/span&gt;

  &lt;span class="nv"&gt;@base_url&lt;/span&gt; &lt;span class="s2"&gt;"https://app.posthog.com"&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;distinct_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;properties&lt;/span&gt; &lt;span class="p"&gt;\\&lt;/span&gt; &lt;span class="p"&gt;%{})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;event:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;distinct_id:&lt;/span&gt; &lt;span class="n"&gt;distinct_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;api_key:&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;properties:&lt;/span&gt; &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_nil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="no"&gt;Logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Posthog API key not set, skipping event: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;Jason&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;base_request&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;url:&lt;/span&gt; &lt;span class="s2"&gt;"/capture"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;json:&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;transform_response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;capture_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;batch:&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;api_key:&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_nil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="no"&gt;Logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Posthog API key not set, skipping batch: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;Jason&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;base_request&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;url:&lt;/span&gt; &lt;span class="s2"&gt;"/capture"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;json:&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;transform_response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;base_request&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;Req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;base_url:&lt;/span&gt; &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;transform_response&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;status:&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="ow"&gt;when&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;transform_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;

  &lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;Application&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:points&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:posthog_api_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;base_url&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="no"&gt;Application&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:points&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:posthog_api_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;@base_url&lt;/span&gt;
      &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  The Synchronous Telemetry Handler
&lt;/h4&gt;

&lt;p&gt;This handler just passes whatever is sent under the &lt;code&gt;[:posthog, :event]&lt;/code&gt; key directly to the capture method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;Points&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;TelemetryHandlers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SimplePosthog&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="ss"&gt;:telemetry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"posthog-events"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:posthog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:event&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="bp"&gt;__MODULE__&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handle_event&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;detach&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="ss"&gt;:telemetry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;detach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"posthog-events"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;handle_event&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="ss"&gt;:posthog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:event&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;measure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_meta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;Posthog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;measure&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To register the handler, I just called &lt;code&gt;Points.TelemetryHandlers.SimplePosthog.attach()&lt;/code&gt; in the initialization method of my application.&lt;/p&gt;

&lt;h4&gt;
  
  
  Sending an Event
&lt;/h4&gt;

&lt;p&gt;With the plumbing in place, all I had to do was send a Telemetry event under the expected key.&lt;/p&gt;

&lt;p&gt;Here's a snippet from the controller that creates a new session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;PointsWeb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SessionController&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;Points&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SessionServer&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;PointsWeb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:controller&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;session_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;generate_new_session_id&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="no"&gt;SessionServer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ensure_started&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="ss"&gt;:telemetry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="ss"&gt;:posthog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:event&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;
      &lt;span class="ss"&gt;event:&lt;/span&gt; &lt;span class="s2"&gt;"session_created"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;distinct_id:&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;properties:&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;
        &lt;span class="s2"&gt;"session_id"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="n"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to:&lt;/span&gt; &lt;span class="sx"&gt;~p"/sessions/#{session_id}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The value passed as the second argument to &lt;code&gt;:telemetry.execute&lt;/code&gt; makes it to PostHog.&lt;/p&gt;

&lt;h4&gt;
  
  
  Reflecting on the Approach
&lt;/h4&gt;

&lt;p&gt;The biggest problem with this naive solution is that the &lt;code&gt;:telemetry.execute&lt;/code&gt; call caused a meaningful, inconsistent delay every time a user created a session. This is because the application had to wait for the registered event handler to execute before the application could proceed, and the event handler made a blocking call to an external service.&lt;/p&gt;

&lt;p&gt;Clearly, there must be a better way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making it Better: Async
&lt;/h3&gt;

&lt;p&gt;As with many other problems faced in Elixir, the solution &lt;em&gt;may&lt;/em&gt; involve a GenServer. Before I get into that, I want to make an important note: I didn't reach for a GenServer until I found that I had a need for a background process with persistent state. It's valuable to try a simple approach before introducing another process to the supervision tree.&lt;/p&gt;

&lt;p&gt;Knowing that the synchronous event handler caused a real delay in the application, I chose to follow the advice from the Telemetry readme:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you need to perform any action that is not immediate, consider offloading the work to a separate process (or a pool of processes) by sending a message.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The new architecture includes a handler that stores the event in memory and periodically flushes its received events to PostHog. Initially, I was going to directly use GenServer state to temporarily store events, but the &lt;code&gt;handle_event&lt;/code&gt; method is called by the process that invokes &lt;code&gt;:telemetry.execute&lt;/code&gt;. To get this event into the GenServer state, I'd have to use &lt;code&gt;GenServer.cast&lt;/code&gt; and cause a bottleneck in the GenServer's mailbox.&lt;/p&gt;

&lt;p&gt;Instead of GenServer state, I realized a write-optimized &lt;a href="https://elixirschool.com/en/lessons/storage/ets"&gt;Ets table&lt;/a&gt; would be a more appropriate temporary storage location. This is natively implemented code within the BEAM virtual machine and scales much better than a single process.&lt;/p&gt;

&lt;p&gt;The new Telemetry handler can be found below. To use it, I added &lt;code&gt;Points.TelemetryReporters.Posthog&lt;/code&gt; to my application supervision tree. When it starts up, it attaches to the existing Telemetry event key and goes to work.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;Points&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;TelemetryReporters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Posthog&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;GenServer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;shutdown:&lt;/span&gt; &lt;span class="mi"&gt;2_000&lt;/span&gt;

  &lt;span class="kn"&gt;require&lt;/span&gt; &lt;span class="no"&gt;Logger&lt;/span&gt;

  &lt;span class="nv"&gt;@default_publish_interval&lt;/span&gt; &lt;span class="mi"&gt;10_000&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;start_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;when&lt;/span&gt; &lt;span class="n"&gt;is_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;GenServer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;__MODULE__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="nv"&gt;@impl&lt;/span&gt; &lt;span class="no"&gt;GenServer&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;table_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;create_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_atom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.Ets"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="no"&gt;Process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:trap_exit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="no"&gt;Logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; Started"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_id&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nv"&gt;@default_publish_interval&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="nv"&gt;@impl&lt;/span&gt; &lt;span class="no"&gt;GenServer&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;handle_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_unix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc_now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="ss"&gt;:millisecond&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:ets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[{{&lt;/span&gt;&lt;span class="ss"&gt;:"$1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:"$2"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="ss"&gt;:&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:"$1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:"$2"&lt;/span&gt;&lt;span class="p"&gt;]}])&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="no"&gt;Posthog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;capture_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
          &lt;span class="ss"&gt;:ets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;select_delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[{{&lt;/span&gt;&lt;span class="ss"&gt;:"$1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:"$2"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="ss"&gt;:&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:"$1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;]}])&lt;/span&gt;

        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
          &lt;span class="no"&gt;Logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;] Failed to flush events to Posthog: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;inspect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:noreply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;@default_publish_interval&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="nv"&gt;@impl&lt;/span&gt; &lt;span class="no"&gt;GenServer&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;terminate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_id&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;Logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;] Stopped with reason &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;inspect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;detach&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="no"&gt;Logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;] Flushing final events to Posthog"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:ets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[{{&lt;/span&gt;&lt;span class="ss"&gt;:"$1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:"$2"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:"$2"&lt;/span&gt;&lt;span class="p"&gt;]}])&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="no"&gt;Posthog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;capture_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;] Final events flushed to Posthog"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;] Failed to flush final events to Posthog"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="ss"&gt;:ets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="ss"&gt;:telemetry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"posthog-events"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:posthog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:event&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="bp"&gt;__MODULE__&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handle_event&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;table_id:&lt;/span&gt; &lt;span class="n"&gt;table_id&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;detach&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="ss"&gt;:telemetry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;detach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"posthog-events"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;handle_event&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="ss"&gt;:posthog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:event&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;measure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_meta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;table_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:table_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="ss"&gt;:ets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="no"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_unix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc_now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="ss"&gt;:millisecond&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;measure&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;create_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="ss"&gt;:ets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:named_table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:duplicate_bag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:public&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:write_concurrency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}])&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every ten seconds, this process checks the Ets table for new events and conditionally posts them to Posthog as a batch operation. This makes heavy use of the third response value of a GenServer lifecycle method: the timeout. After some specified number of milliseconds passes, if no other event is processed by the GenServer, the &lt;code&gt;handle_info&lt;/code&gt; method is called with a message of &lt;code&gt;:timeout&lt;/code&gt;. It's a great way to create a background loop in an Elixir application.&lt;/p&gt;

&lt;p&gt;To avoid losing events when the server shuts down, exits are trapped and the &lt;code&gt;terminate&lt;/code&gt; callback makes a final attempt to send events out. The &lt;code&gt;shutdown&lt;/code&gt; value of &lt;code&gt;2_000&lt;/code&gt; indicates that the process wants two seconds to perform its cleanup task. This behavior is described well &lt;a href="https://hexdocs.pm/elixir/1.16.1/GenServer.html#c:terminate/2"&gt;in the Elixir docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outcome
&lt;/h2&gt;

&lt;p&gt;Now that events are buffered in memory through a write-optimized Ets table, I don't have to worry about the latency penalty of a blocking network call. The real beauty of this approach is that I didn't have to change any of my &lt;code&gt;:telemetry.execute&lt;/code&gt; calls to get this performance improvement. Thanks to the Telemetry abstraction, it just works.&lt;/p&gt;

&lt;p&gt;I'm able to measure the growth of the application I'm working on and may be motivated to continue working on it—knowing that it has a healthy user base that comes back sprint after sprint.&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>analytics</category>
      <category>programming</category>
    </item>
    <item>
      <title>Random Cut Forests</title>
      <dc:creator>Christian Alexander</dc:creator>
      <pubDate>Mon, 07 Aug 2023 13:00:00 +0000</pubDate>
      <link>https://dev.to/christianalexander/random-cut-forests-ff1</link>
      <guid>https://dev.to/christianalexander/random-cut-forests-ff1</guid>
      <description>&lt;p&gt;Anomaly detection is a complicated and well-researched area of computer science. With all of the machine learning hype of recent months, I wanted to build &lt;em&gt;something&lt;/em&gt; using my favorite programming language, Elixir.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq9wk791xs2cimra0fitt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq9wk791xs2cimra0fitt.png" alt="Results"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://livebook.dev/run?url=https%3A%2F%2Fgist.github.com%2FChristianAlexander%2F30174b3f0f921a41d03bb9578e6bed7b" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flivebook.dev%2Fbadge%2Fv1%2Fblack.svg" alt="Run in Livebook"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In 2016, a paper called &lt;a href="https://proceedings.mlr.press/v48/guha16.pdf" rel="noopener noreferrer"&gt;Robust Random Cut Forest Algorithm&lt;/a&gt; was published.&lt;/p&gt;

&lt;p&gt;The Random Cut Forest Algorithm is an unsupervised anomaly detection algorithm that is typically used in Amazon Sagemaker (&lt;a href="https://docs.aws.amazon.com/sagemaker/latest/dg/randomcutforest.html" rel="noopener noreferrer"&gt;docs&lt;/a&gt;). It aims to improve upon the &lt;a href="https://cs.nju.edu.cn/zhouzh/zhouzh.files/publication/icdm08b.pdf" rel="noopener noreferrer"&gt;Isolation Forest Algorithm&lt;/a&gt;. I could have tried my hand at isolation forests, but they’re so 2008&lt;a href="https://youtu.be/4m48GqaOz90?t=178" rel="noopener noreferrer"&gt;.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What makes the random cut forest algorithm different is that it measures the collusive displacement rather than just the depth of a leaf node. This measure represents how many leaves in the tree would be displaced as a result of its insertion or removal. In datasets with irrelevant features, this produces much better results.&lt;/p&gt;

&lt;p&gt;In both algorithms, an ensemble of trees (a forest) is constructed. Each tree provides a value for a given point and they are combined to produce the result.&lt;/p&gt;

&lt;p&gt;The solution in the LiveBook document above is written entirely in Elixir, allowing it to be invoked without &lt;a href="https://www.erlang.org/doc/tutorial/nif.html" rel="noopener noreferrer"&gt;NIFs&lt;/a&gt;. A production-grade solution might involve calling a native implementation, but it’s likely that a more efficient version could still be built in Elixir.&lt;/p&gt;

&lt;h2&gt;
  
  
  How RCF Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Making a tree
&lt;/h3&gt;

&lt;p&gt;A set of multi-dimensional points are introduced to the tree. For simplicity, they will be depicted in two dimensions. In reality, any number of dimensions will do.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F794jz8d69dzti4090ptk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F794jz8d69dzti4090ptk.png" alt="Starting point"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A random cut is made along a random axis, weighted by the span of each dimension’s bounding box. In this case, we randomly choose to make a vertical cut, isolating one point.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwpjo9cm06rd171bb7bbm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwpjo9cm06rd171bb7bbm.png" alt="First cut, 2d"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This can also be represented visually as a decision tree.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzp0elxpmd8cgihvanyno.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzp0elxpmd8cgihvanyno.png" alt="First cut, tree"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We then repeat this process until each point (leaf node) is isolated.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmgf7vku4qh2lghiwfg66.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmgf7vku4qh2lghiwfg66.png" alt="Subsequent cuts, 2d"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frsfb8uhep9j3ay089lfe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frsfb8uhep9j3ay089lfe.png" alt="Subsequent cuts, tree"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Forest
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F486bq48r8toxzxu78j8i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F486bq48r8toxzxu78j8i.png" alt="A forest"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A random cut forest is an ensemble of trees. Each tree is unique and learns something different about the dataset by the points it’s fed and the cuts it chooses.&lt;/p&gt;

&lt;h3&gt;
  
  
  Evaluation
&lt;/h3&gt;

&lt;p&gt;To determine how much of anomaly a datapoint is, we insert it into the tree and calculate a collusive displacement score. This score is calculated across all trees in the forest, and the arithmetic mean is used as the result.&lt;/p&gt;

&lt;p&gt;For example, if we were looking for the orange dot in the tree above, its score from the tree would be 6—the number of nodes that would be displaced if we were to remove it. They would be displaced because the root node would be replaced, and all branches below would be shifted up.&lt;/p&gt;

&lt;p&gt;The displacement score of the red dot in this tree would be 4—the number of leaf nodes under its sibling. Visually, this makes sense. It’s closer to the cluster than the orange point.&lt;/p&gt;

&lt;p&gt;Another tree might have made a different observation, deciding to cut out the red dot before the black one. This would result in a higher score.&lt;/p&gt;

&lt;p&gt;For this reason, it’s important to consider the perspectives of the full forest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sine Wave Anomaly Exercise
&lt;/h2&gt;

&lt;p&gt;In the research paper, the team constructs a sine wave and inserts an anomalous flat section in the middle. They then train a forest on the normal section and present the calculated anomaly scores for each point.&lt;/p&gt;

&lt;p&gt;The linked LiveBook does the same, with some fuzziness applied to the wave for added realism.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5kslmy6h7mcd5hi391or.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5kslmy6h7mcd5hi391or.png" alt="Sine wave"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Shingling the Data
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/W-shingling" rel="noopener noreferrer"&gt;Shingling&lt;/a&gt; is used to turn the time series into something periodic. Adjacent values are inserted in the tree together as multiple dimensions of the same datapoint.&lt;/p&gt;

&lt;p&gt;For example, given a series &lt;code&gt;[1, 2, 3, 4, 5]&lt;/code&gt; and a shingle factor of 3, the tree would receive the following multi-dimensional data points:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[1, 2, 3]
[2, 3, 4]
[3, 4, 5]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shingling is used to represent patterns within the larger dataset. A peak is typically surrounded by lower values, and downward values typically only increase after a valley.&lt;/p&gt;

&lt;h3&gt;
  
  
  Final Results
&lt;/h3&gt;

&lt;p&gt;With 1,000 data points, an anomaly lasting 20 steps, 50 trees containing 256 4-shingled points each, the following result is achieved:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq9wk791xs2cimra0fitt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq9wk791xs2cimra0fitt.png" alt="Results"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The outlier score spikes at the beginning and end of the anomaly. There are also some smaller spikes as a result of the noise applied to the wave.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc4ah1dm041pke11yc5l5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc4ah1dm041pke11yc5l5.png" alt="Distribution of outlier scores"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A histogram of the outlier scores shows that most scores hover around the 4–4.5 region, while a very small number of scores exceed 20—let alone 30. This analysis can be helpful in tuning the parameters of the forest as well as determining an alerting threshold.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The linked LiveBook explores a partial implementation of the approach described in the RCF paper. Along with some visuals, it has helped me build an intuition for how some random choices and a few trees can result in unsupervised anomaly detection.&lt;/p&gt;

&lt;p&gt;If you haven’t already, try out the LiveBook locally and check out the implementation details that didn’t fit in this post:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://livebook.dev/run?url=https%3A%2F%2Fgist.github.com%2FChristianAlexander%2F30174b3f0f921a41d03bb9578e6bed7b" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flivebook.dev%2Fbadge%2Fv1%2Fblack.svg" alt="Run in Livebook"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Citations
&lt;/h2&gt;

&lt;p&gt;S. Guha, N. Mishra, G. Roy, &amp;amp; O. Schrijvers, Robust random cut forest based anomaly detection on streams, in Proceedings of the 33rd International conference on machine learning, New York, NY, 2016 (pp. 2712-2721).&lt;/p&gt;

&lt;p&gt;F. T. Liu, K. M. Ting and Z. -H. Zhou, "Isolation Forest," 2008 Eighth IEEE International Conference on Data Mining, Pisa, Italy, 2008, pp. 413-422, doi: 10.1109/ICDM.2008.17.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>elixir</category>
      <category>algorithms</category>
    </item>
    <item>
      <title>Mitigating Server-Side Request Forgery</title>
      <dc:creator>Christian Alexander</dc:creator>
      <pubDate>Wed, 26 Apr 2023 13:30:00 +0000</pubDate>
      <link>https://dev.to/christianalexander/mitigating-server-side-request-forgery-beg</link>
      <guid>https://dev.to/christianalexander/mitigating-server-side-request-forgery-beg</guid>
      <description>&lt;p&gt;Server-Side Request Forgery (SSRF) vulnerabilities allow an attacker to cause a server application to perform an unintended request. When exploited, the server could leak sensitive internal information or perform dangerous actions. Because this vulnerability depends on the capabilities of the server application, the potential impact of an attack can vary.&lt;/p&gt;

&lt;p&gt;Webhooks are among the most common features that introduce SSRF vulnerabilities to applications. They combine arbitrary user input (the webhook URL) with the ability to make requests from the backend. It’s important to consider this threat when building and operating webhook systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The attack
&lt;/h2&gt;

&lt;p&gt;For the purposes of this post, imagine we have a web application that is able to perform outbound requests to a user-configured endpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud credential leak
&lt;/h3&gt;

&lt;p&gt;Every AWS compute resource has a special internal service, called the &lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html"&gt;Instance Metadata Service (IMDS)&lt;/a&gt;. This endpoint provides network information, initialization scripts, and even &lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#instance-metadata-security-credentials"&gt;temporary AWS access keys&lt;/a&gt;. All of this is conveniently hosted at &lt;code&gt;http://169.254.169.254&lt;/code&gt; . The feature is great for avoiding hard-coded keys but comes at a cost—by default, it is available to every process on the server.&lt;/p&gt;

&lt;p&gt;The scenario for exploiting this is simple: the user provides a URL of &lt;code&gt;http://169.254.169.254/latest/meta-data/iam/security-credentials/role-name&lt;/code&gt; to the application, then look at the response. It should look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Code"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"LastUpdated"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-04-26T16:39:16Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AWS-HMAC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"AccessKeyId"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ASIAIOSFODNN7EXAMPLE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"SecretAccessKey"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Token"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Expiration"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2017-05-17T15:09:54Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the key in hand, the attacker can perform any action that the compute instance is permitted to do. Because AWS IAM can be complicated, some organizations provide more permission than is the server needs—using &lt;code&gt;*&lt;/code&gt; wildcards. Occasionally, the same IAM role will be used across staging and production environments.&lt;/p&gt;

&lt;p&gt;Even if the server isn’t hosted in AWS, there are similar endpoints in all major cloud providers. This includes &lt;a href="https://cloud.google.com/compute/docs/metadata/overview"&gt;Google Cloud&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service?tabs=windows"&gt;Azure&lt;/a&gt;, and &lt;a href="https://docs.digitalocean.com/products/droplets/how-to/retrieve-droplet-metadata/"&gt;DigitalOcean&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Internal endpoints
&lt;/h3&gt;

&lt;p&gt;Mature organizations might rely on private networking to protect internal resources, asserting that a VPN connection is required for access. Unfortunately, web servers tend to sit on the same network.&lt;/p&gt;

&lt;p&gt;Imagine there’s an intranet site containing trade secrets sitting on the internal network, available for all employees to access. By convincing an application server to perform requests against the site, an attacker can steal all of those secrets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Filesystem access
&lt;/h3&gt;

&lt;p&gt;Web application servers tend to contain secrets of their own: source code, configuration files, and user uploads. An attacker providing a request URL with the &lt;code&gt;file://&lt;/code&gt; scheme may be able to access any file visible to the web server process. This might already be locked down depending on the library being used to perform requests from the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mitigating the vulnerability
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://deliver.dev"&gt;Deliver&lt;/a&gt;, the webhook monitoring system I've been working on, is written in Elixir. Naturally, the examples will also use Elixir. The same mitigations will likely be available in any technology, but specific package recommendations may not apply.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preventing non-HTTP(S) URIs
&lt;/h3&gt;

&lt;p&gt;The following example shows how the &lt;code&gt;URI.parse/1&lt;/code&gt; &lt;a href="https://hexdocs.pm/elixir/1.12/URI.html#parse/1"&gt;method&lt;/a&gt; breaks a URI into a struct.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"https://elixir-lang.org/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;authority:&lt;/span&gt; &lt;span class="s2"&gt;"elixir-lang.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;fragment:&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;host:&lt;/span&gt; &lt;span class="s2"&gt;"elixir-lang.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;path:&lt;/span&gt; &lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;port:&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;query:&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;scheme:&lt;/span&gt; &lt;span class="s2"&gt;"https"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;userinfo:&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A first pass at SSRF protection might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;permit_uri?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheme&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"https"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By locking out unacceptable schemes, we ensure that the filesystem can't directly be accessed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preventing the instance metadata IP address
&lt;/h3&gt;

&lt;p&gt;Another layer of protection that could be valuable is excluding &lt;code&gt;169.254.169.254&lt;/code&gt;, as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;permit_uri?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;parsed_uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;parsed_uri&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheme&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"https"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt;
    &lt;span class="n"&gt;parsed_uri&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"169.254.169.254"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One interesting problem with this solution is that public DNS endpoints can return any IP address they'd like in response to an A record query. For &lt;strong&gt;example&lt;/strong&gt;, this would be completely legal in DNS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; nslookup malicious.example.com
Server:     1.1.1.1
Address:    1.1.1.1#53

Non-authoritative answer:
Name:   malicious.example.com
Address: 169.254.169.254
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If an attacker arranged for a request to &lt;code&gt;http://malicious.example.com/latest/meta-data/iam/security-credentials/role-name&lt;/code&gt;, it would be treated as though the IMDS IP address were provided to begin with.&lt;/p&gt;

&lt;h3&gt;
  
  
  Looking up DNS hostnames
&lt;/h3&gt;

&lt;p&gt;Erlang's built-in &lt;code&gt;:inet_dns&lt;/code&gt; module can be used directly in order to perform DNS lookups. On the Erlang side, IPv4 addresses are expressed as tuples with four elements, and strings used for networking have to be of type &lt;code&gt;charlist&lt;/code&gt; instead of &lt;code&gt;binary&lt;/code&gt;. Clearly, this implementation is getting complicated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;SSRFProtection&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nv"&gt;@disallowed_hosts&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;169&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;254&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;169&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;254&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;resolve_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;to_charlist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="ss"&gt;:inet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse_address&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="ss"&gt;:inet_res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;permit_uri?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;parsed_uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;parsed_uri&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheme&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"https"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt;
      &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;any?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;resolve_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parsed_uri&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;&amp;amp;1&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;@disallowed_hosts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By running the user-provided URIs through the &lt;code&gt;permit_uri?/1&lt;/code&gt; method of &lt;code&gt;SSRFProtection&lt;/code&gt;, we can prevent this class of requests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;SSRFProtection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permit_uri?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"https://google.com/something"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;true&lt;/span&gt;

&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;SSRFProtection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permit_uri?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"https://169.254.169.254/something"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;false&lt;/span&gt;

&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;SSRFProtection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permit_uri?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"https://malicious.example.com/something"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;false&lt;/span&gt;

&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;SSRFProtection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permit_uri?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"file://C:&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;inetpub&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;wwwroot&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;secret.html"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Perform validation on every request
&lt;/h3&gt;

&lt;p&gt;While there is some level of protection provided by validating a user-provided URI when it's first submitted, it's not the best we can do. DNS is not static. An attacker could submit a URI that previously resolved to &lt;code&gt;123.123.123.123&lt;/code&gt;, but update the record to &lt;code&gt;169.254.169.254&lt;/code&gt; after the application validates it.&lt;/p&gt;

&lt;p&gt;This is why it's important to run &lt;code&gt;permit_uri?/1&lt;/code&gt; (or its equivalent) for each request the server makes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using SafeURL
&lt;/h2&gt;

&lt;p&gt;As indicated above, this implementation is messy and not great. It describes the concepts, but it's probably not the most production-ready solution. Luckily, there's an open-source Elixir package called &lt;a href="https://hexdocs.pm/safeurl/SafeURL.html"&gt;SafeURL&lt;/a&gt; that encapsulates SSRF-protecting validation into a single module.&lt;/p&gt;

&lt;p&gt;Wherever &lt;code&gt;SSRFProtection.permit_uri?/1&lt;/code&gt; was used, we can instead use &lt;code&gt;SafeURL.allowed?/1&lt;/code&gt;. This library has a built-in list of IP addresses, ranges, and schemes that are a great default. Depending on your use case, it can even perform &lt;code&gt;GET&lt;/code&gt; requests for your application through an optional &lt;code&gt;HTTPoison&lt;/code&gt; integration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Similar libraries
&lt;/h3&gt;

&lt;p&gt;There are similar libraries for SSRF protection in most programming languages. A couple that appear to be reputable are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ruby: &lt;a href="https://rubygems.org/gems/ssrf_filter"&gt;&lt;code&gt;ssrf_filter&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Python: &lt;a href="https://pypi.org/project/advocate/"&gt;&lt;code&gt;advocate&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Taking it further
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Don't follow redirects
&lt;/h3&gt;

&lt;p&gt;Some request libraries automatically follow HTTP 301 and 302 redirects. Ensure this behavior is disabled when performing requests against user-provided URLs. A malicious actor could host &lt;code&gt;https://happy-looking-site.com&lt;/code&gt;, where it redirects to &lt;code&gt;169.254.169.254&lt;/code&gt;. If the web application follows the redirect, the attack will succeed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security groups
&lt;/h3&gt;

&lt;p&gt;Make sure to configure security groups or the cloud firewall for the hosting provider of the web application in a way that limits access to what's strictly necessary. If it doesn't make sense for the application to reach a specific internal service, don't allow it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Proxy
&lt;/h3&gt;

&lt;p&gt;An additional layer of security that some companies choose to implement is an HTTP egress proxy. These typically are instances of &lt;a href="https://aws.amazon.com/blogs/networking-and-content-delivery/providing-controlled-internet-access-through-centralised-proxy-servers-using-aws-fargate-and-privatelink/"&gt;Squid&lt;/a&gt;, &lt;a href="https://blog.palantir.com/using-envoy-for-egress-traffic-8524d10b5ee2"&gt;Envoy&lt;/a&gt;, or a cloud provider &lt;a href="https://aws.amazon.com/blogs/networking-and-content-delivery/migrating-from-squid-web-proxy-to-aws-network-firewall/"&gt;hosted solution&lt;/a&gt;. When outbound requests are made by the web application, they go through the proxy. This proxy should be configured with no permission to reach back into the private network and ideally will prevent requests to bad IPs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Security is tricky to get right and can have disastrous side-effects when done incorrectly. Attackers will poke holes in every line of defense, which is why it's important to consider your threat model and establish a &lt;a href="https://csrc.nist.gov/glossary/term/defense_in_depth"&gt;defense-in-depth&lt;/a&gt; strategy.&lt;/p&gt;

&lt;p&gt;While writing a solution in-house can seem reasonable, there are tricky edge cases that might be solved in a well-accepted open-source package. Look to the community for the best defense.&lt;/p&gt;

&lt;p&gt;Go and protect your application from SSRF vulnerabilities!&lt;/p&gt;

</description>
      <category>security</category>
      <category>elixir</category>
    </item>
    <item>
      <title>Efficient bidirectional infinite scroll in Phoenix LiveView</title>
      <dc:creator>Christian Alexander</dc:creator>
      <pubDate>Fri, 14 Oct 2022 13:35:30 +0000</pubDate>
      <link>https://dev.to/christianalexander/efficient-bidirectional-infinite-scroll-in-phoenix-liveview-3epd</link>
      <guid>https://dev.to/christianalexander/efficient-bidirectional-infinite-scroll-in-phoenix-liveview-3epd</guid>
      <description>&lt;p&gt;In this post, I'll share the progression of my implementation of an infinite scrolling UI in Phoenix LiveView—from naive to efficient. Follow along with the example implementation in &lt;a href="https://github.com/ChristianAlexander/liveview_bidirectional_scroll_demo" rel="noopener noreferrer"&gt;this repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;Phoenix LiveView makes it easy to create interactive web apps without having to write much (if any) frontend code. The server sends diffs over a websocket to update the client's UI. Data can be presented to the user without page refreshes or polling.&lt;/p&gt;

&lt;p&gt;A few weeks ago, I set out to add an alert list page to Deliver, a webhook monitoring system I've been building (&lt;a href="https://deliver.dev" rel="noopener noreferrer"&gt;check it out here&lt;/a&gt;). &lt;/p&gt;

&lt;p&gt;As the page is scrolled, older alerts are loaded into the table. Critically, new alerts are added to the top of the page as they occur, backed by a Phoenix PubSub subscription.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F24zgtiwjejnnhndxygw7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F24zgtiwjejnnhndxygw7.png" alt="Screenshot from Deliver"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;To demonstrate the implementation of this project, I've created &lt;a href="https://github.com/ChristianAlexander/liveview_bidirectional_scroll_demo" rel="noopener noreferrer"&gt;a new LiveView application&lt;/a&gt; that contains a simplified data model that simulates what had to be built in Deliver. It uses a &lt;a href="https://github.com/ChristianAlexander/liveview_bidirectional_scroll_demo/commit/57fe82366c28bf3c6d6f564387401ba6ba3b9011" rel="noopener noreferrer"&gt;background process&lt;/a&gt; (a &lt;a href="https://hexdocs.pm/elixir/1.14/GenServer.html" rel="noopener noreferrer"&gt;GenServer&lt;/a&gt;) to create events and publish them over &lt;a href="https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.html" rel="noopener noreferrer"&gt;Phoenix PubSub&lt;/a&gt;, storing the events in a database for later retrieval.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 1: The Memory Hog
&lt;/h2&gt;

&lt;p&gt;The first implementation of the alerts page was really easy to implement, but it kept on using more and more memory during sustained sessions.&lt;/p&gt;

&lt;p&gt;First, I created a simple table view that loaded five alerts from the &lt;code&gt;alerts&lt;/code&gt; table (&lt;a href="https://github.com/ChristianAlexander/liveview_bidirectional_scroll_demo/commit/6af99f99057488bd8383d9e0657036f4bd0bedd6" rel="noopener noreferrer"&gt;link to related commit&lt;/a&gt;).&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;

&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;BidirectionalScrollWeb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Live&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;ScrollDemoLive&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;BidirectionalScrollWeb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:live_view&lt;/span&gt;

  &lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;BidirectionalScroll&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Alerts&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;alerts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Alerts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;list_alerts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;limit:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;alerts:&lt;/span&gt; &lt;span class="n"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="sx"&gt;~H""&lt;/span&gt;&lt;span class="s2"&gt;"
    &amp;lt;h1&amp;gt;Alerts&amp;lt;/h1&amp;gt;
    &amp;lt;table&amp;gt;
      &amp;lt;thead&amp;gt;
        &amp;lt;tr&amp;gt;
          &amp;lt;th&amp;gt;ID&amp;lt;/th&amp;gt;
          &amp;lt;th&amp;gt;Started At&amp;lt;/th&amp;gt;
          &amp;lt;th&amp;gt;Resolved At&amp;lt;/th&amp;gt;
        &amp;lt;/tr&amp;gt;
      &amp;lt;/thead&amp;gt;
      &amp;lt;tbody&amp;gt;
        &amp;lt;%= for alert &amp;lt;- @alerts do %&amp;gt;
          &amp;lt;tr&amp;gt;
            &amp;lt;td&amp;gt;&amp;lt;%= alert.id %&amp;gt;&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;&amp;lt;%= alert.started_at %&amp;gt;&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;&amp;lt;%= alert.resolved_at %&amp;gt;&amp;lt;/td&amp;gt;
          &amp;lt;/tr&amp;gt;
        &amp;lt;% end %&amp;gt;
      &amp;lt;/tbody&amp;gt;
    &amp;lt;/table&amp;gt;
    """&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Next, I added a "Load More" button to the bottom of the page, using &lt;code&gt;phx-click&lt;/code&gt; and a &lt;code&gt;handle_event&lt;/code&gt; callback (&lt;a href="https://github.com/ChristianAlexander/liveview_bidirectional_scroll_demo/commit/a27f6c82effbc48713911a56961a378691326c41" rel="noopener noreferrer"&gt;link to related commit&lt;/a&gt;). Additional alerts are added to the existing ones in a large list, assigned to the socket and rendered in the table.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="err"&gt;

&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/bidirectional_scroll_web/live/scroll_demo_live.ex
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/bidirectional_scroll_web/live/scroll_demo_live.ex
&lt;/span&gt;&lt;span class="p"&gt;@@ -11,6 +11,18 @@&lt;/span&gt; defmodule BidirectionalScrollWeb.Live.ScrollDemoLive do
     {:ok, socket}
   end
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+  def handle_event("load-more", _value, socket) do
+    alerts = socket.assigns.alerts
+
+    oldest_loaded_alert = Enum.min_by(alerts, &amp;amp; &amp;amp;1.started_at, NaiveDateTime)
+    older_alerts = Alerts.list_alerts(started_before: oldest_loaded_alert.started_at, limit: 5)
+
+    alerts = alerts ++ older_alerts
+    socket = assign(socket, alerts: alerts)
+
+    {:noreply, socket}
+  end
+
&lt;/span&gt;   def render(assigns) do
     ~H"""
     &amp;lt;h1&amp;gt;Alerts&amp;lt;/h1&amp;gt;
&lt;span class="p"&gt;@@ -32,6 +44,7 @@&lt;/span&gt; defmodule BidirectionalScrollWeb.Live.ScrollDemoLive do
         &amp;lt;% end %&amp;gt;
       &amp;lt;/tbody&amp;gt;
     &amp;lt;/table&amp;gt;
&lt;span class="gi"&gt;+    &amp;lt;button phx-click="load-more"&amp;gt;Load More&amp;lt;/button&amp;gt;
&lt;/span&gt;     """
   end
 end
&lt;span class="err"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Finally, I wired up Phoenix PubSub to broadcast when alerts are created and updated. The subscription in the scrolling UI causes two &lt;code&gt;handle_info&lt;/code&gt; callbacks to be invoked. When &lt;code&gt;:alert_created&lt;/code&gt; is received, the new alert is prepended to the list that is assigned to the socket. &lt;code&gt;:alert_updated&lt;/code&gt; uses &lt;code&gt;Enum.map&lt;/code&gt; to update the matching alert by ID. (&lt;a href="https://github.com/ChristianAlexander/liveview_bidirectional_scroll_demo/commit/49adc52e344057821b7a9267e5a31c36de0ada9a" rel="noopener noreferrer"&gt;Link to related commit&lt;/a&gt;)&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;handle_info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="ss"&gt;:alert_created&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;alerts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alerts&lt;/span&gt;

    &lt;span class="n"&gt;alerts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;alert&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;alerts:&lt;/span&gt; &lt;span class="n"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:noreply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;handle_info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="ss"&gt;:alert_updated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;alert_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;alerts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alerts&lt;/span&gt;

    &lt;span class="n"&gt;alerts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt;
        &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;id:&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="n"&gt;alert_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;alert&lt;/span&gt;
        &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;alerts:&lt;/span&gt; &lt;span class="n"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:noreply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;At this point, the alerts page &lt;em&gt;technically&lt;/em&gt; works. However, in production this will end up consuming tons of memory. Every connected session will retain a copy of all alerts in an ever-increasing list, never being garbage collected.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5g322neig5tjpj274mxr.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5g322neig5tjpj274mxr.gif" alt="initial implementation working"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is one of the big downsides of naive Phoenix LiveView implementations: in order to automatically produce efficient diffs to send to the browser, the server must keep its assigned values in the socket so it can re-render.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Luckily, this is not the only way to build a list view.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 2: Out of Order
&lt;/h2&gt;

&lt;p&gt;Phoenix LiveView has a concept called &lt;a href="https://hexdocs.pm/phoenix_live_view/dom-patching.html" rel="noopener noreferrer"&gt;temporary assigns&lt;/a&gt; to solve the memory consumption issue encountered in the first attempt.&lt;/p&gt;

&lt;p&gt;To use temporary assigns, we have to add a &lt;code&gt;phx-update&lt;/code&gt; attribute to the list's container, along with providing a DOM ID to uniquely identify the container on the page. This allows the LiveView client script to prepend and append to the container's contents. Each child element also requires an ID to perform updates on items that have already been rendered. (&lt;a href="https://github.com/ChristianAlexander/liveview_bidirectional_scroll_demo/commit/913683f56c694298048d4ecf54b0bdba1a668f80" rel="noopener noreferrer"&gt;Link to related commit&lt;/a&gt;)&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="err"&gt;

&lt;/span&gt;&lt;span class="p"&gt;@@ -61,9 +48,9 @@&lt;/span&gt; defmodule BidirectionalScrollWeb.Live.ScrollDemoLive do
           &amp;lt;th&amp;gt;Resolved At&amp;lt;/th&amp;gt;
         &amp;lt;/tr&amp;gt;
       &amp;lt;/thead&amp;gt;
&lt;span class="gd"&gt;-      &amp;lt;tbody&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+      &amp;lt;tbody id="alert-list" phx-update="prepend"&amp;gt;
&lt;/span&gt;         &amp;lt;%= for alert &amp;lt;- @alerts do %&amp;gt;
&lt;span class="gd"&gt;-          &amp;lt;tr&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+          &amp;lt;tr id={"alert-#{alert.id}"} &amp;gt;
&lt;/span&gt;             &amp;lt;td&amp;gt;&amp;lt;%= alert.id %&amp;gt;&amp;lt;/td&amp;gt;
             &amp;lt;td&amp;gt;&amp;lt;%= alert.started_at %&amp;gt;&amp;lt;/td&amp;gt;
             &amp;lt;td&amp;gt;&amp;lt;%= alert.resolved_at %&amp;gt;&amp;lt;/td&amp;gt;
&lt;span class="err"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;For now, we are hard-coding &lt;code&gt;phx-update&lt;/code&gt; to &lt;code&gt;prepend&lt;/code&gt;. Any alert that hasn't yet been received by the frontend will be added to the top of the list.&lt;/p&gt;

&lt;p&gt;Along with this &lt;code&gt;render&lt;/code&gt; update, we have to start using temporary assigns:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="err"&gt;

&lt;/span&gt;&lt;span class="p"&gt;@@ -6,33 +6,20 @@&lt;/span&gt; defmodule BidirectionalScrollWeb.Live.ScrollDemoLive do  
   def mount(_params, _session, socket) do
     if connected?(socket) do
       Phoenix.PubSub.subscribe(BidirectionalScroll.PubSub, "alerts")
     end
     alerts = Alerts.list_alerts(limit: 5)
&lt;span class="err"&gt;
&lt;/span&gt;     socket = assign(socket, alerts: alerts)
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-    {:ok, socket}
&lt;/span&gt;&lt;span class="gi"&gt;+    {:ok, socket, temporary_assigns: [alerts: []]}
&lt;/span&gt;   end
&lt;span class="err"&gt;
&lt;/span&gt;   def handle_info({:alert_created, alert}, socket) do
&lt;span class="gd"&gt;-    alerts = socket.assigns.alerts
-
-    alerts = [alert | alerts]
-    socket = assign(socket, alerts: alerts)
-
&lt;/span&gt;&lt;span class="gi"&gt;+    socket = assign(socket, alerts: [alert])
&lt;/span&gt;     {:noreply, socket}
   end
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-  def handle_info({:alert_updated, %{id: alert_id} = alert}, socket) do
-    alerts = socket.assigns.alerts
-
-    alerts =
-      Enum.map(alerts, fn
-        %{id: ^alert_id} -&amp;gt; alert
-        a -&amp;gt; a
-      end)
-
-    socket = assign(socket, alerts: alerts)
-
&lt;/span&gt;&lt;span class="gi"&gt;+  def handle_info({:alert_updated, alert}, socket) do
+    socket = assign(socket, alerts: [alert])
&lt;/span&gt;     {:noreply, socket}
   end
&lt;span class="err"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Adding the &lt;code&gt;temporary_assigns&lt;/code&gt; to the mount response causes the assigns with corresponding keys to be reset to the default value after every render. In this case, we reset &lt;code&gt;socket.assigns.alerts&lt;/code&gt; to &lt;code&gt;[]&lt;/code&gt; after every render. By resetting the value, we allow the runtime to garbage collect the &lt;code&gt;Alert&lt;/code&gt; instances that were being referenced as members of the &lt;code&gt;alerts&lt;/code&gt; list.&lt;/p&gt;

&lt;p&gt;This update ends up simplifying the &lt;code&gt;handle_info&lt;/code&gt; callback implementations. Since a render runs every time a callback returns, we can set the &lt;code&gt;alerts&lt;/code&gt; list to a list only containing the new or updated value. In the case of &lt;code&gt;:alert_created&lt;/code&gt;, there will be no element with a matching ID and it will follow the &lt;code&gt;phx-update&lt;/code&gt; behavior—in this case prepending to the list. As for &lt;code&gt;:alert_updated&lt;/code&gt;, a matching element should be found in the DOM. The matching element will simply be replaced.&lt;/p&gt;

&lt;p&gt;The "Load More" button causes the LiveView process to crash now, since its previous implementation looked at the list of assigned alerts to figure out the &lt;code&gt;started_at&lt;/code&gt; value of the earliest alert on the page. Since the list is empty, we have no record of the oldest loaded alert. To fix this, we can add (and maintain) a single datetime in the socket. (&lt;a href="https://github.com/ChristianAlexander/liveview_bidirectional_scroll_demo/commit/29a623d8a5d7d66857084a56d4feead6f24ddf39" rel="noopener noreferrer"&gt;Link to related commit&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;At this point, the page prepends items as expected, but the "Load More" button ends up prepending values that should be appended.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0it1an833d0jrwyvp6os.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0it1an833d0jrwyvp6os.gif" alt="out of order"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 3: It Works Again
&lt;/h2&gt;

&lt;p&gt;At the end of the second attempt, we had an implementation that was close to feature parity with the naive memory hog, but it had one major flaw: we were only prepending to the list.&lt;/p&gt;

&lt;p&gt;The update behavior is controlled by the &lt;code&gt;phx-update&lt;/code&gt; attribute of the container element. There's nothing keeping us from setting this to &lt;code&gt;append&lt;/code&gt; or &lt;code&gt;prepend&lt;/code&gt; dynamically. It turns out that the solution to this problem is quite simple:&lt;br&gt;
(&lt;a href="https://github.com/ChristianAlexander/liveview_bidirectional_scroll_demo/commit/2c299aa3a72bf5a3a7d3ad0b5ce463739036536d" rel="noopener noreferrer"&gt;Link to relevant commit&lt;/a&gt;)&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="err"&gt;

&lt;/span&gt;&lt;span class="p"&gt;@@ -6,28 +6,39 @@&lt;/span&gt; defmodule BidirectionalScrollWeb.Live.ScrollDemoLive do  
   def mount(_params, _session, socket) do
     if connected?(socket) do
       Phoenix.PubSub.subscribe(BidirectionalScroll.PubSub, "alerts")
     end
     alerts = Alerts.list_alerts(limit: 5)
     oldest_loaded_alert = Enum.min_by(alerts, &amp;amp; &amp;amp;1.started_at, NaiveDateTime)
&lt;span class="err"&gt;
&lt;/span&gt;     socket =
       assign(socket,
         alerts: alerts,
&lt;span class="gd"&gt;-        oldest_alert_started_at: oldest_loaded_alert.started_at
&lt;/span&gt;&lt;span class="gi"&gt;+        oldest_alert_started_at: oldest_loaded_alert.started_at,
+        update_direction: "append"
&lt;/span&gt;       )
&lt;span class="err"&gt;
&lt;/span&gt;     {:ok, socket, temporary_assigns: [alerts: []]}
   end
&lt;span class="err"&gt;
&lt;/span&gt;   def handle_info({:alert_created, alert}, socket) do
&lt;span class="gd"&gt;-    socket = assign(socket, alerts: [alert])
&lt;/span&gt;&lt;span class="gi"&gt;+    socket =
+      assign(socket,
+        alerts: [alert],
+        update_direction: "prepend"
+      )
+
&lt;/span&gt;     {:noreply, socket}
   end
&lt;span class="err"&gt;
&lt;/span&gt;   def handle_info({:alert_updated, alert}, socket) do
&lt;span class="gd"&gt;-    socket = assign(socket, alerts: [alert])
&lt;/span&gt;&lt;span class="gi"&gt;+    socket =
+      assign(socket,
+        alerts: [alert],
+        update_direction: "prepend"
+      )
+
&lt;/span&gt;     {:noreply, socket}
   end
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;@@ -34,13 +45,14 @@&lt;/span&gt; defmodule BidirectionalScrollWeb.Live.ScrollDemoLive do
   def handle_event("load-more", _value, socket) do
     oldest_alert_started_at = socket.assigns.oldest_alert_started_at
     alerts = Alerts.list_alerts(started_before: oldest_alert_started_at, limit: 5)
     oldest_loaded_alert = Enum.min_by(alerts, &amp;amp; &amp;amp;1.started_at, NaiveDateTime)
&lt;span class="err"&gt;
&lt;/span&gt;     socket =
       assign(socket,
         alerts: alerts,
&lt;span class="gd"&gt;-        oldest_alert_started_at: oldest_loaded_alert.started_at
&lt;/span&gt;&lt;span class="gi"&gt;+        oldest_alert_started_at: oldest_loaded_alert.started_at,
+        update_direction: "append"
&lt;/span&gt;       )
&lt;span class="err"&gt;
&lt;/span&gt;     {:noreply, socket}
&lt;span class="p"&gt;@@ -57,7 +69,7 @@&lt;/span&gt; defmodule BidirectionalScrollWeb.Live.ScrollDemoLive do
           &amp;lt;th&amp;gt;Resolved At&amp;lt;/th&amp;gt;
         &amp;lt;/tr&amp;gt;
       &amp;lt;/thead&amp;gt;
&lt;span class="gd"&gt;-      &amp;lt;tbody id="alert-list" phx-update="prepend"&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+      &amp;lt;tbody id="alert-list" phx-update={@update_direction}&amp;gt;
&lt;/span&gt;         &amp;lt;%= for alert &amp;lt;- @alerts do %&amp;gt;
           &amp;lt;tr id={"alert-#{alert.id}"} &amp;gt;
             &amp;lt;td&amp;gt;&amp;lt;%= alert.id %&amp;gt;&amp;lt;/td&amp;gt;
&lt;span class="err"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;In this change, we bind the &lt;code&gt;phx-update&lt;/code&gt; attribute of the &lt;code&gt;alert-list&lt;/code&gt; container element to the assigns value of &lt;code&gt;update_direction&lt;/code&gt;. Whenever we receive a new alert, we set the direction to &lt;code&gt;append&lt;/code&gt;. Whenever the "Load More" button is clicked, we set it to &lt;code&gt;append&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We now have a working implementation that matches the first attempt, but with substantially less per-connection memory consumption on the server side.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu5utabfwb2dyj1na3mu7.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu5utabfwb2dyj1na3mu7.gif" alt="it works again"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: Automatically Load More
&lt;/h2&gt;

&lt;p&gt;Most infinite scrolling implementations don't require the user to click a button to load more content. We can do the same using &lt;a href="https://hexdocs.pm/phoenix_live_view/js-interop.html#client-hooks-via-phx-hook" rel="noopener noreferrer"&gt;LiveView client hooks&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Hooks allow the client to execute JavaScript functions during the lifecycle of any element with a corresponding &lt;code&gt;phx-hook&lt;/code&gt; attribute. (&lt;a href="https://github.com/ChristianAlexander/liveview_bidirectional_scroll_demo/commit/c82d40e62709529ffdc8a2a9c2b86632c9ccfe2e" rel="noopener noreferrer"&gt;Link to related commit&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;In this case, we will add a hook named &lt;code&gt;InfiniteScrollButton&lt;/code&gt; that will simulate a click of the button whenever it enters the browser's viewport. This code replaces the existing &lt;code&gt;new LiveSocket&lt;/code&gt; call in &lt;code&gt;assets/js/app.js&lt;/code&gt;.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;liveSocket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LiveSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/live&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;_csrf_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;csrfToken&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;InfiniteScrollButton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;loadMore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isIntersecting&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nf"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IntersectionObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadMore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// window by default&lt;/span&gt;
            &lt;span class="na"&gt;rootMargin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nf"&gt;beforeDestroy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unobserve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Finally, we have to add the &lt;code&gt;phx-hook&lt;/code&gt; attribute (and a DOM ID to help with tracking) to the rendered "Load More" button to cause it to click itself whenever it enters the page:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="err"&gt;

&lt;/span&gt;&lt;span class="p"&gt;@@ -79,7 +79,7 @@&lt;/span&gt; defmodule BidirectionalScrollWeb.Live.ScrollDemoLive do
         &amp;lt;% end %&amp;gt;
       &amp;lt;/tbody&amp;gt;
     &amp;lt;/table&amp;gt;
&lt;span class="gd"&gt;-    &amp;lt;button phx-click="load-more"&amp;gt;Load More&amp;lt;/button&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+    &amp;lt;button id="alerts-load-more" phx-hook="InfiniteScrollButton" phx-click="load-more"&amp;gt;Load More&amp;lt;/button&amp;gt;
&lt;/span&gt;     """
   end
 end
&lt;span class="err"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;That's it! We now have a memory-efficient, bidirectional infinite scrolling view that automatically keeps elements up-to-date whenever they are sent over PubSub.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3m0wk2fgc785qkddixjz.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3m0wk2fgc785qkddixjz.gif" alt="Final"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Phoenix LiveView makes it simple to build applications that work. When performance and scaling issues come up, escape hatches such as temporary assigns and the client library can save the day. As a final step, interactivity can be added through hooks and (not seen here) &lt;a href="https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.JS.html" rel="noopener noreferrer"&gt;JS commands&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;LiveView is easily the most productive full-stack solution I've ever encountered, and it's a joy to get to use it in projects like Deliver (which, again, you should &lt;a href="https://deliver.dev" rel="noopener noreferrer"&gt;check out&lt;/a&gt; if you have a webhook system).&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>phoenix</category>
      <category>liveview</category>
    </item>
    <item>
      <title>Adding soft delete to a Phoenix Commanded (CQRS) API</title>
      <dc:creator>Christian Alexander</dc:creator>
      <pubDate>Wed, 01 Jun 2022 22:41:41 +0000</pubDate>
      <link>https://dev.to/christianalexander/adding-soft-delete-to-a-phoenix-commanded-cqrs-api-516h</link>
      <guid>https://dev.to/christianalexander/adding-soft-delete-to-a-phoenix-commanded-cqrs-api-516h</guid>
      <description>&lt;h2&gt;
  
  
  Goal
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Implement a soft delete in the API from part one, allowing items to be restored after deletion.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Follow along with what I learned while iterating on a project named &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded"&gt;&lt;code&gt;todo_backend_commanded&lt;/code&gt;&lt;/a&gt;. Its &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/compare/FirstPost...SecondPost"&gt;git history&lt;/a&gt; shows the steps involved to implement soft delete and restore functionality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;In the previous post, I converted a vanilla Phoenix API to CQRS with Commanded.&lt;/p&gt;

&lt;p&gt;This application writes to the database using the &lt;a href="https://hexdocs.pm/commanded_ecto_projections/Commanded.Projections.Ecto.html"&gt;&lt;code&gt;commanded_ecto_projections&lt;/code&gt;&lt;/a&gt; hex package, which subscribes to events and projects their state into tables managed by the &lt;a href="https://hexdocs.pm/ecto/getting-started.html"&gt;Ecto database library&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Since the core data model of this application is an append-only (immutable) log, the events can be replayed and the read model can be dramatically changed using existing data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updating the aggregate
&lt;/h2&gt;

&lt;p&gt;(&lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/fa0dfeb53992c3f45fdd2f94294f6b3b71d5fac5"&gt;Link to relevant commit&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;The first step is to add a &lt;code&gt;deleted_at&lt;/code&gt; field to the &lt;code&gt;Todo&lt;/code&gt; aggregate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; defmodule TodoBackend.Todos.Aggregates.Todo do
   defstruct [
     :uuid,
     :title,
     :completed,
&lt;span class="gd"&gt;-    :order
&lt;/span&gt;&lt;span class="gi"&gt;+    :order,
+    deleted_at: nil
&lt;/span&gt;   ]
&lt;span class="err"&gt;
&lt;/span&gt;   ...
&lt;span class="gd"&gt;-  def apply(%Todo{uuid: uuid}, %TodoDeleted{uuid: uuid}) do
-    nil
-  end
&lt;/span&gt;&lt;span class="gi"&gt;+  def apply(%Todo{uuid: uuid} = todo, %TodoDeleted{uuid: uuid}) do
+    %Todo{todo | deleted_at: DateTime.utc_now()}
+  end
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An aggregate is a struct representing the state of an object in an application. In Commanded, aggregates are hydrated in-memory from dispatched events. The aggregate struct is used to determine the side-effects of a command, if the command is to be accepted at all.&lt;/p&gt;

&lt;p&gt;This diff causes the aggregate's &lt;code&gt;deleted_at&lt;/code&gt; field to be set as the datetime when the event log is hydrated. We'll come back to this, since it would be preferable to capture the time the todo item was deleted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Aside: aggregates sound slow
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;If an aggregate has to be hydrated from its events every time it is used, how is this not a terrible performance burden?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In practice, an individual aggregate shouldn't have many events. How many interactions could one todo item possibly have? If it turns out certain items have large event logs, there are a couple of levers that can be pulled in Commanded.&lt;/p&gt;

&lt;h4&gt;
  
  
  Lifespans
&lt;/h4&gt;

&lt;p&gt;(&lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/8c5d60e6dabdc9b41587783f4359d2730a6199eb"&gt;Link to relevant commit&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;So, I &lt;del&gt;lied&lt;/del&gt; told a half-truth about aggregates. They are not hydrated in-memory for &lt;em&gt;every&lt;/em&gt; command / event. In reality, aggregates are implemented with &lt;a href="https://hexdocs.pm/elixir/1.13/GenServer.html"&gt;&lt;code&gt;GenServer&lt;/code&gt;&lt;/a&gt; each caching their state and being managed under the commanded application's supervision tree (ultimately by a &lt;a href="https://hexdocs.pm/elixir/1.13/DynamicSupervisor.html"&gt;&lt;code&gt;DynamicSupervisor&lt;/code&gt;&lt;/a&gt; called &lt;a href="https://github.com/commanded/commanded/blob/b497d8cfde386c272902809511f8373d03652684/lib/commanded/aggregates/supervisor.ex"&gt;&lt;code&gt;Commanded.Aggregates.Supervisor&lt;/code&gt;&lt;/a&gt;, to be specific).&lt;/p&gt;

&lt;p&gt;Since aggregates are cached as long-running processes, we only have to fetch each event once.&lt;/p&gt;

&lt;p&gt;The timeout of an aggregate process can be configured via an implementation of the &lt;a href="https://hexdocs.pm/commanded/1.3.1/Commanded.Aggregates.AggregateLifespan.html"&gt;&lt;code&gt;AggregateLifespan&lt;/code&gt;&lt;/a&gt; behaviour. The default lifespan, &lt;a href="https://hexdocs.pm/commanded/1.3.1/Commanded.Aggregates.DefaultLifespan.html#content"&gt;&lt;code&gt;DefaultLifespan&lt;/code&gt;&lt;/a&gt;, sets an infinite timeout unless an exception is raised.&lt;/p&gt;

&lt;p&gt;Depending on the number of aggregate instances in a live system, this could introduce another problem—these processes could stay alive for the life of the server. That sounds like an &lt;a href="https://en.wikipedia.org/wiki/Out_of_memory"&gt;OOM&lt;/a&gt; waiting to happen.&lt;/p&gt;

&lt;p&gt;To address this, I &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/8c5d60e6dabdc9b41587783f4359d2730a6199eb"&gt;implemented&lt;/a&gt; a lifespan for the Todo aggregate that keeps aggregates around for a minute—unless they receive the &lt;code&gt;TodoDeleted&lt;/code&gt; event. Being able to pattern match and implement this within the context of an application means the timeouts can reflect the specific details of your domain.&lt;/p&gt;

&lt;h4&gt;
  
  
  Snapshots
&lt;/h4&gt;

&lt;p&gt;For aggregates with &lt;em&gt;many&lt;/em&gt; events, Commanded has &lt;a href="https://hexdocs.pm/commanded/aggregates.html#aggregate-state-snapshots"&gt;aggregate state snapshots&lt;/a&gt;. These can be configured by adding a &lt;code&gt;snapshotting&lt;/code&gt; section to the Commanded application in &lt;code&gt;config/config.exs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There are two options per aggregate: &lt;code&gt;snapshot_every&lt;/code&gt; and &lt;code&gt;snapshot_version&lt;/code&gt;. &lt;code&gt;snapshot_every&lt;/code&gt; is used to cause snapshots to be created after a certain number of events. This value acts as an upper bound of events that must be evaluated when hydrating an aggregate state. &lt;code&gt;snapshot_version&lt;/code&gt; is a non-negative integer that should be updated every time the aggregate is updated. It is used to invalidate old snapshots that may be missing new fields or were created with old &lt;code&gt;apply&lt;/code&gt; methods.&lt;/p&gt;

&lt;p&gt;Snapshots can be valuable in applications with long event logs, but didn't seem to be worthwhile in this todo API. If we were using snapshots, the addition of the &lt;code&gt;deleted_at&lt;/code&gt; field would have required an increment of &lt;code&gt;snapshot_version&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To learn more about aggregate performance, check out &lt;a href="https://q-the-hacker.com/post/how-we-reduced-memory-usage-in-an-application-built-with-elixir-and-commanded"&gt;this blog post&lt;/a&gt; where an organization dramatically improved the performance and footprint of their Commanded application through lifespans and snapshots.&lt;/p&gt;

&lt;h2&gt;
  
  
  Effective dating the delete event
&lt;/h2&gt;

&lt;p&gt;(&lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/8677c8603c3df38acec01ede4f51e8a7ad664d43"&gt;Link to relevant commit&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;As I noted when adding the &lt;code&gt;deleted_at&lt;/code&gt; field to the aggregate, it's not appropriate to use the datetime when the event is applied. This would cause the &lt;code&gt;deleted_at&lt;/code&gt; value to be different every time the service is deployed or the aggregate genserver is restarted. It would be much better to capture the datetime when the event is &lt;em&gt;created&lt;/em&gt;, so it can be persisted in the event log.&lt;/p&gt;

&lt;p&gt;First, I &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/8677c8603c3df38acec01ede4f51e8a7ad664d43#diff-d18cc26ef219037313f4afd1ab6ddb0dbbada23795085bf8848aac4512067980R5"&gt;added&lt;/a&gt; a &lt;code&gt;datetime&lt;/code&gt; field to the &lt;code&gt;TodoDeleted&lt;/code&gt; event struct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; defmodule TodoBackend.Todos.Events.TodoDeleted do
   @derive Jason.Encoder
   defstruct [
&lt;span class="gd"&gt;-    :uuid
&lt;/span&gt;&lt;span class="gi"&gt;+    :uuid,
+    :datetime
&lt;/span&gt;   ]
&lt;span class="p"&gt;end
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, I &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/8677c8603c3df38acec01ede4f51e8a7ad664d43#diff-7b526edfd483136debc93c34aff9f6b4005bab384d15889ada645e3887707929R35"&gt;populated&lt;/a&gt; this value in the &lt;code&gt;execute&lt;/code&gt; method of the todo aggregate, when the &lt;code&gt;DeleteTodo&lt;/code&gt; command received. Since &lt;code&gt;execute&lt;/code&gt; is called when a command is dispatched, this will be set to the datetime when the delete endpoint is called.&lt;/p&gt;

&lt;p&gt;I also &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/8677c8603c3df38acec01ede4f51e8a7ad664d43#diff-7b526edfd483136debc93c34aff9f6b4005bab384d15889ada645e3887707929R86-R87"&gt;updated&lt;/a&gt; the &lt;code&gt;apply&lt;/code&gt; method to use the datetime from the event, instead of the current datetime. This value will come from the event log.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; defmodule TodoBackend.Todos.Aggregates.Todo do
   ...
   def execute(%Todo{uuid: uuid}, %DeleteTodo{uuid: uuid}) do
&lt;span class="gd"&gt;-    %TodoDeleted{uuid: uuid}
&lt;/span&gt;&lt;span class="gi"&gt;+    %TodoDeleted{uuid: uuid, datetime: DateTime.utc_now()}
&lt;/span&gt;   end
&lt;span class="err"&gt;
&lt;/span&gt;   ...
&lt;span class="gd"&gt;-  def apply(%Todo{uuid: uuid} = todo, %TodoDeleted{uuid: uuid}) do
-    %Todo{todo | deleted_at: DateTime.utc_now()}
&lt;/span&gt;&lt;span class="gi"&gt;+  def apply(%Todo{uuid: uuid} = todo, %TodoDeleted{uuid: uuid, datetime: effective_datetime}) do
+    %Todo{todo | deleted_at: effective_datetime}
&lt;/span&gt;   end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Read your writes
&lt;/h3&gt;

&lt;p&gt;The log stores events in JSON format. Since JSON does not have a native datetime format, the &lt;code&gt;Commanded.Serialization.JsonDecoder&lt;/code&gt; protocol must be implemented on the event struct.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;TodoBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Todos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Events&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;TodoDeleted&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;

  &lt;span class="k"&gt;defimpl&lt;/span&gt; &lt;span class="no"&gt;Commanded&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Serialization&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;JsonDecoder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;for:&lt;/span&gt; &lt;span class="no"&gt;TodoDeleted&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nv"&gt;@doc&lt;/span&gt; &lt;span class="sd"&gt;"""
    Parse the datetime included in the aggregate state
    """&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(%&lt;/span&gt;&lt;span class="no"&gt;TodoDeleted&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;TodoDeleted&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;datetime:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;

      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_iso8601&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;TodoDeleted&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="ss"&gt;datetime:&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What about existing events?
&lt;/h3&gt;

&lt;p&gt;This is fine, but what about all of those events that are already in the log &lt;em&gt;without datetimes&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;Since the log is append-only and immutable, the way to extend an event definition is to add a fallback value for the new field.&lt;/p&gt;

&lt;p&gt;In this case, I &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/8677c8603c3df38acec01ede4f51e8a7ad664d43#diff-d18cc26ef219037313f4afd1ab6ddb0dbbada23795085bf8848aac4512067980R10-R25"&gt;modified&lt;/a&gt; the JSON decoder method—substituting a &lt;code&gt;nil&lt;/code&gt; value for the current datetime. Since the value was never written to old events, the actual deletion time is lost. The best thing we can do is provide &lt;em&gt;some&lt;/em&gt; value and ensure we write a datetime on all &lt;em&gt;future&lt;/em&gt; events.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; defmodule TodoBackend.Todos.Events.TodoDeleted do
   ...
&lt;span class="err"&gt;
&lt;/span&gt;   defimpl Commanded.Serialization.JsonDecoder, for: TodoDeleted do
     @doc """
     Parse the datetime included in the aggregate state
     """
     def decode(%TodoDeleted{} = state) do
       %TodoDeleted{datetime: datetime} = state
&lt;span class="gd"&gt;-
-      {:ok, dt, _} = DateTime.from_iso8601(datetime)
-
-      %TodoDeleted{state | datetime: dt}
&lt;/span&gt;&lt;span class="gi"&gt;+
+      if datetime == nil do
+        %TodoDeleted{state | datetime: DateTime.utc_now()}
+      else
+        {:ok, dt, _} = DateTime.from_iso8601(datetime)
+
+        %TodoDeleted{state | datetime: dt}
+      end
+    end
&lt;/span&gt;   end
 end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Updating the read model
&lt;/h2&gt;

&lt;p&gt;(&lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/fb148f5e740b15c19674ed5ab9a4dbe8bca3fae5"&gt;Link to relevant commit&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;In a production environment it would be best to create a new database table and projector containing a &lt;code&gt;deleted_at&lt;/code&gt; field, deploy the application to build the table, update callers to use the new table, and then delete the old table. This is safe to do because all writes happen in the event layer—the read model is read-only.&lt;/p&gt;

&lt;p&gt;In some cases, it could also be reasonable to update the projector such that the data can be migrated in-place. This seemed like more effort than it was worth for a blog post, so I opted for the dangerous option: blow away the table and rebuild it with an updated projector.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preparing the database
&lt;/h3&gt;

&lt;p&gt;First, I generated a migration to add the &lt;code&gt;deleted_at&lt;/code&gt; column to the &lt;code&gt;todos&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix ecto.gen.migration add_deleted_at_to_todo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the generated migration file, I &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/fb148f5e740b15c19674ed5ab9a4dbe8bca3fae5#diff-771a05b347391dd50e0b2f4f0a814471418dd4c9f28281e1a0bd160b4babbb5cR1-R11"&gt;added&lt;/a&gt; the column and an index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;TodoBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Migrations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;AddDeletedAtToTodo&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Ecto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;change&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;alter&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:todos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;add&lt;/span&gt; &lt;span class="ss"&gt;:deleted_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:naive_datetime_usec&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;create&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:todos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:deleted_at&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, I &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/fb148f5e740b15c19674ed5ab9a4dbe8bca3fae5#diff-a24b2206c0045791d9a61f95bfb498c81f3bc6fe265b9176c3f255350ff93adcR12"&gt;added&lt;/a&gt; the field to the projection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; defmodule TodoBackend.Todos.Projections.Todo do
   ...
   schema "todos" do
     field :completed, :boolean, default: false
     field :title, :string
     field :order, :integer, default: 0
&lt;span class="gi"&gt;+    field :deleted_at, :naive_datetime_usec, default: nil
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;     timestamps()
   end
 end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After running &lt;code&gt;mix ecto.migrate&lt;/code&gt;, the database side is ready to go.&lt;/p&gt;

&lt;h3&gt;
  
  
  Updating the projector
&lt;/h3&gt;

&lt;p&gt;The projector &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/fb148f5e740b15c19674ed5ab9a4dbe8bca3fae5#diff-667c477b155244629e3c37f1e798562904356911efc9038052d6e10bb5f42705R27-R38"&gt;update&lt;/a&gt; is very simple: instead of deleting the row from the table, just update the &lt;code&gt;deleted_at&lt;/code&gt; column.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; defmodule TodoBackend.Todos.Projectors.Todo do
   ...
&lt;span class="gd"&gt;-  project(%TodoDeleted{uuid: uuid}, _, fn multi -&amp;gt;
-    Ecto.Multi.delete(multi, :todo, fn _ -&amp;gt; %Todo{uuid: uuid} end)
&lt;/span&gt;&lt;span class="gi"&gt;+  project(%TodoDeleted{uuid: uuid, datetime: effective_datetime}, _, fn multi -&amp;gt;
+    case Repo.get(Todo, uuid) do
+      nil -&amp;gt;
+        multi
+
+      todo -&amp;gt;
+        Ecto.Multi.update(
+          multi,
+          :todo,
+          Todo.delete_changeset(todo, %{deleted_at: effective_datetime})
+        )
+    end
+  end)
&lt;/span&gt;   ...
 end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/fb148f5e740b15c19674ed5ab9a4dbe8bca3fae5#diff-a24b2206c0045791d9a61f95bfb498c81f3bc6fe265b9176c3f255350ff93adcR22-R25"&gt;added&lt;/a&gt; a &lt;code&gt;delete_changeset&lt;/code&gt; to the &lt;code&gt;Todo&lt;/code&gt; projection to represent a specific desired change. It ensures we only update the &lt;code&gt;deleted_at&lt;/code&gt; column. This is referenced in the new &lt;code&gt;TodoDeleted&lt;/code&gt; projector method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;TodoBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Todos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Projections&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;delete_changeset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt; &lt;span class="p"&gt;\\&lt;/span&gt; &lt;span class="p"&gt;%{})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;todo&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:deleted_at&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Forcefully re-populating the database
&lt;/h3&gt;

&lt;p&gt;Like I mentioned before, it's a terrible idea to truncate a database in production. Here's how I did it locally to explore Commanded. It's possible this could also be done by updating the &lt;code&gt;name&lt;/code&gt; value provided to the &lt;code&gt;use Commanded.Projections.Ecto&lt;/code&gt; call in &lt;code&gt;TodoBackend.Todos.Projections.Todo&lt;/code&gt; and restarting the server.&lt;/p&gt;

&lt;p&gt;If there is no existing data in the log, this step can be skipped.&lt;/p&gt;

&lt;p&gt;In an iex shell (&lt;code&gt;iex -S mix phx.server&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Delete the projected values&lt;/span&gt;
&lt;span class="no"&gt;TodoBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delete_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;TodoBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Todos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Projections&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Remove the version tracking entry for the ecto projection&lt;/span&gt;
&lt;span class="no"&gt;TodoBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(%&lt;/span&gt;&lt;span class="no"&gt;TodoBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Todos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Projectors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;ProjectionVersion&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;projection_name:&lt;/span&gt; &lt;span class="s2"&gt;"Todos.Projectors.Todo"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;Commanded&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Handler&lt;/span&gt;
&lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;Commanded&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Registration&lt;/span&gt;

&lt;span class="c1"&gt;# Trigger a reset of the projector&lt;/span&gt;
&lt;span class="n"&gt;registry_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;TodoBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;App&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Todos.Projectors.Todo"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;projector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Registration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;whereis_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;TodoBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;App&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;registry_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;projector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:reset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Hiding deleted items
&lt;/h3&gt;

&lt;p&gt;Now that deleted items are present in the &lt;code&gt;todos&lt;/code&gt; table, it's important to &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/fb148f5e740b15c19674ed5ab9a4dbe8bca3fae5#diff-afaa01c994784ec0c0ffa28cdf2a145b7d22723c401f3cf4ba9880e0dc835fc6R25-R50"&gt;update&lt;/a&gt; the &lt;code&gt;Todos&lt;/code&gt; context module to filter out deleted items. This can be done in the context without needing any updates to the controller, since all access is encapsulated. As I noted in the previous post, this is a really valuable property of the DDD-inspired approach of modern Phoenix applications.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; defmodule TodoBackend.Todos do
   ...
&lt;span class="err"&gt;
&lt;/span&gt;   def list_todos do
&lt;span class="gd"&gt;-    Repo.all(Todo)
&lt;/span&gt;&lt;span class="gi"&gt;+    from(t in Todo,
+      where: is_nil(t.deleted_at)
+    )
+    |&amp;gt; Repo.all()
&lt;/span&gt;   end
&lt;span class="err"&gt;
&lt;/span&gt;   ...
&lt;span class="gd"&gt;-  def get_todo!(uuid), do: Repo.get_by!(Todo, uuid: uuid)
&lt;/span&gt;&lt;span class="gi"&gt;+  def get_todo!(uuid) do
+    from(t in Todo,
+      where: is_nil(t.deleted_at)
+    )
+    |&amp;gt; Repo.get_by!(uuid: uuid)
+  end
&lt;/span&gt; end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Where are we, now?
&lt;/h3&gt;

&lt;p&gt;Soft deletes have been implemented. The read model has been reconstructed from existing data, including previously-deleted items.&lt;/p&gt;

&lt;p&gt;In CQRS applications, data never is really &lt;em&gt;deleted&lt;/em&gt;—at most, it just gets removed from the read model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not legal advice&lt;/strong&gt; on handling GDPR: some applications may choose to encrypt log entries with a per-entity encryption key in order to satisfy permanent data deletion requests. To fully delete data, the encryption key could be discarded. This would cause the events to be unreadable if they were ever replayed. Some special handling would be necessary to prevent the application from crashing on GDPR-deleted entities. This is very clearly out of scope for this blog post, but a very interesting concept.&lt;/p&gt;

&lt;p&gt;Michiel Rook wrote two posts on this topic, which can be found &lt;a href="https://www.michielrook.nl/2017/11/forget-me-please-event-sourcing-gdpr/"&gt;here&lt;/a&gt; and &lt;a href="https://www.michielrook.nl/2017/11/event-sourcing-gdpr-follow-up/"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  New feature: restore deleted items
&lt;/h2&gt;

&lt;p&gt;Now that soft deletes are implemented, it's time to write the new feature: un-delete!&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementation
&lt;/h3&gt;

&lt;p&gt;(&lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/ee10fa40052c0ab73f561bc6b1734339281820be"&gt;Link to relevant commit&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Let's quickly walk through the code changes required to implement the restoration of deleted todo items:&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/ee10fa40052c0ab73f561bc6b1734339281820be#diff-83580821993b9a15859cbea46472f80d5fb5b9edf30329066de09987e3d3a8edR1-R5"&gt;Create&lt;/a&gt; a &lt;code&gt;RestoreTodo&lt;/code&gt; command
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;TodoBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Todos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Commands&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;RestoreTodo&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;defstruct&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="ss"&gt;:uuid&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/ee10fa40052c0ab73f561bc6b1734339281820be#diff-3151baf357eed2a693f93a3728de3207dbf5beb9adb5a4ece13e6c50c3d77a36R1-R6"&gt;Create&lt;/a&gt; a &lt;code&gt;TodoRestored&lt;/code&gt; event
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;TodoBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Todos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Events&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;TodoRestored&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nv"&gt;@derive&lt;/span&gt; &lt;span class="no"&gt;Jason&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Encoder&lt;/span&gt;
  &lt;span class="k"&gt;defstruct&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="ss"&gt;:uuid&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Update the aggregate
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; defmodule TodoBackend.Todos.Aggregates.Todo do
   ...
&lt;span class="gi"&gt;+  def execute(%Todo{uuid: uuid}, %RestoreTodo{uuid: uuid}) do
+    %TodoRestored{uuid: uuid}
+  end
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;   ...
&lt;span class="gi"&gt;+  def apply(%Todo{uuid: uuid} = todo, %TodoRestored{uuid: uuid}) do
+    %Todo{todo | deleted_at: nil}
+  end
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;   ...
&lt;span class="p"&gt;end
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/ee10fa40052c0ab73f561bc6b1734339281820be#diff-cb8c1b25d823c46caace7a55ba7ebf5695977d01d72023fbad445d046f0091ddR7-R11"&gt;Route&lt;/a&gt; the command to the aggregate
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; defmodule TodoBackend.Router do
   ...
&lt;span class="gd"&gt;-  dispatch([CreateTodo, DeleteTodo, UpdateTodo],
&lt;/span&gt;&lt;span class="gi"&gt;+  dispatch([CreateTodo, DeleteTodo, UpdateTodo, RestoreTodo],
&lt;/span&gt;    to: Todo,
    identity: :uuid,
    lifespan: Todo
  )
&lt;span class="p"&gt;end
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/ee10fa40052c0ab73f561bc6b1734339281820be#diff-667c477b155244629e3c37f1e798562904356911efc9038052d6e10bb5f42705R42-R51"&gt;Update&lt;/a&gt; the projector
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; defmodule TodoBackend.Todos.Projectors.Todo do
   ...
&lt;span class="gi"&gt;+  project(%TodoRestored{uuid: uuid}, _, fn multi -&amp;gt;
+    case Repo.get(Todo, uuid) do
+      nil -&amp;gt;
+        multi
+
+      todo -&amp;gt;
+        Ecto.Multi.update(multi, :todo, Todo.delete_changeset(todo, %{deleted_at: nil}))
+    end
+  end)
&lt;/span&gt; end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, we have implemented everything required for restoring deleted todo items, except for actually dispatching the &lt;code&gt;RestoreTodo&lt;/code&gt; command.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding the API method
&lt;/h3&gt;

&lt;p&gt;(&lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/3ccdff11562a20a0447769dec2355c40ee7529e9"&gt;Link to relevant commit&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;To dispatch the &lt;code&gt;RestoreTodo&lt;/code&gt; command, I added &lt;code&gt;PUT /api/todos/:id/restore&lt;/code&gt; to the API.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/3ccdff11562a20a0447769dec2355c40ee7529e9#diff-afaa01c994784ec0c0ffa28cdf2a145b7d22723c401f3cf4ba9880e0dc835fc6R125-R134"&gt;Add&lt;/a&gt; a method to the context module
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;TodoBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Todos&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;restore_todo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;RestoreTodo&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;uuid:&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="ss"&gt;:ok&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="no"&gt;App&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;consistency:&lt;/span&gt; &lt;span class="ss"&gt;:strong&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_todo!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/3ccdff11562a20a0447769dec2355c40ee7529e9#diff-292b7a1898df5bb867f0195e27cbebe2c66e9fc85bb188aee5d7501bba2c8705R28-R33"&gt;Add&lt;/a&gt; a method to the controller
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;TodoBackendWeb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;TodoController&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;restore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="no"&gt;Todos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;restore_todo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"show.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;todo:&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/3ccdff11562a20a0447769dec2355c40ee7529e9#diff-4ae4e029f3ef4d2cc48d41b90baa205d0a374da25c5ee2f2ff754de5ea726806R13"&gt;Register&lt;/a&gt; the route
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; defmodule TodoBackendWeb.Router do
   ...
&lt;span class="err"&gt;
&lt;/span&gt;   scope "/api", TodoBackendWeb do
     pipe_through :api
&lt;span class="err"&gt;
&lt;/span&gt;     resources "/todos", TodoController
     delete "/todos", TodoController, :delete_all
&lt;span class="gi"&gt;+    put "/todos/:id/restore", TodoController, :restore
&lt;/span&gt;   end
 end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Validation
&lt;/h3&gt;

&lt;p&gt;For the first time in this system, let's look at how we can use aggregate state and the &lt;code&gt;execute&lt;/code&gt; command handler to prevent deleting already-deleted items (and restoring non-deleted items).&lt;/p&gt;

&lt;p&gt;Command validation is a core property of CQRS designs. Once events are emitted, they are never changed. The only way to enforce validations is to accept or reject commands.&lt;/p&gt;

&lt;p&gt;The first validation I'll add is that items which are currently deleted can not be deleted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; defmodule TodoBackend.Todos.Aggregates.Todo do
   ...
&lt;span class="gd"&gt;-  def execute(%Todo{uuid: uuid}, %DeleteTodo{uuid: uuid}) do
&lt;/span&gt;&lt;span class="gi"&gt;+  def execute(%Todo{uuid: uuid, deleted_at: nil}, %DeleteTodo{uuid: uuid}) do
&lt;/span&gt;     %TodoDeleted{uuid: uuid, datetime: DateTime.utc_now()}
   end
&lt;span class="err"&gt;
&lt;/span&gt;   ...
&lt;span class="gi"&gt;+  def execute(%Todo{}, %DeleteTodo{}) do
+    {:error, "Can not delete todo that is already deleted"}
+  end
&lt;/span&gt; end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern-matched method will only emit a &lt;code&gt;TodoDeleted&lt;/code&gt; event when the existing aggregate state contains a &lt;code&gt;deleted_at&lt;/code&gt; value of &lt;code&gt;nil&lt;/code&gt;. In all other cases, it will fall through to the implementation that returns an &lt;code&gt;{:error, message}&lt;/code&gt; tuple.&lt;/p&gt;

&lt;p&gt;In a production system, structured errors should be returned in order to provide API clients with useful responses. Currently, this implementation just causes a 500.&lt;/p&gt;

&lt;p&gt;The other validation will prevent restoring non-deleted items. It reads nearly identically to the first one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; defmodule TodoBackend.Todos.Aggregates.Todo do
   ...
&lt;span class="gi"&gt;+  def execute(%Todo{deleted_at: nil}, %RestoreTodo{}) do
+    {:error, "Can only restore deleted todos"}
+  end
+
&lt;/span&gt;  def execute(%Todo{uuid: uuid}, %RestoreTodo{uuid: uuid}) do
    %TodoRestored{uuid: uuid}
  end
&lt;span class="p"&gt;end
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In this post, I shared how the CQRS implementation of a todo list API made it simple to add soft-delete and restore functionality.&lt;/p&gt;

&lt;p&gt;I added validations and showed how to up-convert existing events to ensure they are compatible with the evolution of a Commanded application.&lt;/p&gt;

&lt;p&gt;In most designs, this would probably not be possible unless a table tracking extension is being used in an ORM. Even with change tracking enabled through extensions like &lt;a href="https://rubygems.org/gems/paper_trail"&gt;paper trail&lt;/a&gt; or &lt;a href="https://django-simple-history.readthedocs.io/en/latest/"&gt;Django simple history&lt;/a&gt;, it can be tricky to restore deleted entities. Object tracking would need to have been enabled &lt;em&gt;before&lt;/em&gt; it is needed to ensure the data is still around to be restored.&lt;/p&gt;

&lt;p&gt;When an immutable, append-only log is the source of truth for an application, it can be updated to meet requirements that were not known at the onset of the project.&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>phoenix</category>
      <category>commanded</category>
      <category>cqrs</category>
    </item>
    <item>
      <title>Using CQRS in a simple Phoenix API with Commanded</title>
      <dc:creator>Christian Alexander</dc:creator>
      <pubDate>Tue, 10 May 2022 14:01:49 +0000</pubDate>
      <link>https://dev.to/christianalexander/using-cqrs-in-a-simple-phoenix-api-with-commanded-364k</link>
      <guid>https://dev.to/christianalexander/using-cqrs-in-a-simple-phoenix-api-with-commanded-364k</guid>
      <description>&lt;p&gt;Despite being a fan of event sourcing and seeing the clear benefits of the approach, I never built anything from scratch. This weekend, I finally decided to break this study cycle and do something practical.&lt;/p&gt;

&lt;p&gt;Follow along with what I learned while implementing a project named &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded"&gt;&lt;code&gt;todo_backend_commanded&lt;/code&gt;&lt;/a&gt;.&lt;br&gt;
Its &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commits/FirstPost"&gt;git history&lt;/a&gt; reflects the process of migrating from a vanilla Phoenix API to an event sourced solution.&lt;/p&gt;
&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;I have been curious about the concepts of &lt;a href="https://docs.microsoft.com/en-us/azure/architecture/patterns/event-sourcing"&gt;event sourcing&lt;/a&gt; and &lt;a href="https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs"&gt;CQRS&lt;/a&gt; for a while—obsessively reading books like &lt;a href="https://www.pragprog.com/titles/egmicro/practical-microservices/"&gt;Practical Microservices (Garofolo)&lt;/a&gt; and &lt;a href="https://www.oreilly.com/library/view/architecture-patterns-with/9781492052197/"&gt;Architecture Patterns with Python (Percival, Gregory)&lt;/a&gt;, along with documentation for libraries like &lt;a href="https://www.sequent.io/"&gt;Sequent (Ruby)&lt;/a&gt;, &lt;a href="https://hexdocs.pm/commanded/Commanded.html"&gt;Commanded (Elixir)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Whenever the topic comes up in conversation—something that happens more often than one might expect—I share a link to Kickstarter's &lt;a href="https://kickstarter.engineering/event-sourcing-made-simple-4a2625113224"&gt;post&lt;/a&gt; on how they used event sourcing to launch d.rip and suggest it wouldn't be too hard to follow their example and give it a try.&lt;/p&gt;
&lt;h2&gt;
  
  
  What did I do?
&lt;/h2&gt;

&lt;p&gt;As noted in my &lt;a href="https://christianalexander.com/2017/09/02/trying-out-asp-dot-net-core/"&gt;ASP.NET Core post from 2017&lt;/a&gt; (&lt;em&gt;wow, I should write more often&lt;/em&gt;), there is a project called &lt;a href="https://www.todobackend.com/"&gt;Todo-Backend&lt;/a&gt; that provides tests gradually guiding its users toward a full implementation of a backend API for a todo list app.&lt;/p&gt;

&lt;p&gt;It's a very familiar API that can be implemented in a few minutes with the &lt;a href="https://www.phoenixframework.org/"&gt;Phoenix Framework&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In fact, the &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/e58b3b5eb95dcf7302ac2d665b65855afd8d9d0b"&gt;first commit&lt;/a&gt; of this post's repository does just that. Most of it is the result of just two commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix phx.new todo_backend_commanded &lt;span class="nt"&gt;--app&lt;/span&gt; todo_backend &lt;span class="nt"&gt;--no-assets&lt;/span&gt; &lt;span class="nt"&gt;--no-html&lt;/span&gt; &lt;span class="nt"&gt;--no-gettext&lt;/span&gt; &lt;span class="nt"&gt;--no-dashboard&lt;/span&gt; &lt;span class="nt"&gt;--no-live&lt;/span&gt; &lt;span class="nt"&gt;--no-mailer&lt;/span&gt;

mix phx.gen.json Todos Todo todos title:string completed:boolean
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a testiment to the value and productivity of Phoenix, but the resulting code is just basic CRUD. The views are tied 1:1 with their database-backed &lt;a href="https://hexdocs.pm/ecto"&gt;Ecto&lt;/a&gt; schemas. One thing to note is that Phoenix generates DDD-style contexts. This is unlike Rails, which would produce a typical ActiveRecord sprawl: bloated models directly being accessed and lazily queried across the entire application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phoenix produces a single interface for each bounded context, making it a great framework for experimenting with a swap of the persistence layer.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Commanded
&lt;/h3&gt;

&lt;p&gt;The Commanded hex package is a fabulous CQRS library &lt;a href="https://github.com/commanded/commanded/wiki/Companies-using-Commanded"&gt;used by some real companies&lt;/a&gt; in production, but it doesn't have a great on-ramp.&lt;/p&gt;

&lt;p&gt;There's an example application called &lt;a href="https://github.com/slashdotdash/conduit"&gt;Conduit&lt;/a&gt;, which is the source code for an eBook's project, but its guidance is not up to date and the book itself starts with account / user management (a pretty advanced domain). The other resource outside of the package documentation is a 20 minute &lt;a href="https://www.youtube.com/watch?v=S3f6sAXa3-c"&gt;conference talk&lt;/a&gt; from 2018.&lt;/p&gt;

&lt;h3&gt;
  
  
  Initializing
&lt;/h3&gt;

&lt;p&gt;To get started with Commanded, I &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/c923c978108528ca0d6490c38a4a15950758b75b"&gt;installed the hex package&lt;/a&gt; and &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/d9877f27ceb65371c762dbe69596edccd6fac5a5"&gt;created two modules&lt;/a&gt; within the TodoBackend application, &lt;code&gt;App&lt;/code&gt; and &lt;code&gt;EventStore&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;TodoBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;App&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Commanded&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;otp_app:&lt;/span&gt; &lt;span class="ss"&gt;:todo_backend&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;TodoBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;EventStore&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;EventStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;otp_app:&lt;/span&gt; &lt;span class="ss"&gt;:todo_backend&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both of these modules rely on macros exposed by Commanded and EventStore. The Commanded &lt;a href="https://hexdocs.pm/commanded/Commanded.Application.html"&gt;documentation&lt;/a&gt; uses the name &lt;code&gt;Application&lt;/code&gt;, but this didn't make much sense to me, since TodoBackend already had an &lt;code&gt;Application&lt;/code&gt; module for the &lt;a href="https://elixir-lang.org/getting-started/mix-otp/supervisor-and-application.html"&gt;supervision tree&lt;/a&gt;. Initially, I even thought that I was supposed to add the &lt;code&gt;Commanded.Application&lt;/code&gt; macro to the supervisor. Once that didn't work, I renamed it to &lt;code&gt;App&lt;/code&gt; and decided that would be the module to house my dispatching and routing.&lt;/p&gt;

&lt;p&gt;Since I already had a Postgres database running, I decided to use &lt;a href="https://github.com/commanded/eventstore"&gt;&lt;code&gt;EventStore&lt;/code&gt;&lt;/a&gt; rather than installing and babysitting EventStoreDB. To initialize the database and tables, I ran &lt;code&gt;mix event_store.init&lt;/code&gt; and &lt;code&gt;mix event_store.create&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  First command: create todo
&lt;/h3&gt;

&lt;p&gt;(&lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/dc6e9fb7f6b704211a47819d4e9e398c4a237d98"&gt;Link to relevant commit&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;CQRS libraries such as Commanded have a concept of &lt;a href="https://martinfowler.com/bliki/DDD_Aggregate.html"&gt;aggregates&lt;/a&gt;, which are the core objects of a domain. With the simple data model of this API, I created a single aggregate to represent a todo. Its initial definition looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;TodoBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Todos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Aggregates&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;defstruct&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="ss"&gt;:uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;:completed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;:order&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An aggregate starts out as a plain struct. Once we have an aggregate, we can layer on commands and events.&lt;/p&gt;

&lt;p&gt;A command represents a caller's intent to have the system respond to some proposed action. It can be accepted or rejected. When accepted, a command produces one or more events. These represent the &lt;em&gt;fact&lt;/em&gt; that something &lt;em&gt;did&lt;/em&gt; happen. They tend to be named in the past tense.&lt;/p&gt;

&lt;p&gt;The first action I created in this system is &lt;code&gt;CreateTodo&lt;/code&gt;, which is defined as a struct with the same fields as the &lt;code&gt;Todo&lt;/code&gt; aggregate. I also created a &lt;code&gt;TodoCreated&lt;/code&gt; event struct—also containing the same fields.&lt;/p&gt;

&lt;p&gt;To decide how to process a command, the &lt;a href="https://hexdocs.pm/commanded/aggregates.html#command-functions"&gt;&lt;code&gt;execute/2&lt;/code&gt;&lt;/a&gt; method is called on the aggregate module, where the first argument is the previous aggregate state (if any exists), and the second argument is the command. If this returns a value that is not an &lt;code&gt;{:error, something}&lt;/code&gt; tuple, the return value will be interpreted as one or more events to be committed to the log.&lt;/p&gt;

&lt;p&gt;In action, the the &lt;code&gt;Todo&lt;/code&gt; aggregate implementation includes the following two methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(%&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;uuid:&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;CreateTodo&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;TodoCreated&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;uuid:&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;title:&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;completed:&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;order:&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(%&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;TodoCreated&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;todo&lt;/span&gt;
        &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="ss"&gt;uuid:&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;title:&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;completed:&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;order:&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, Commanded seems to be a complicated system where the same struct has to be defined many times. The initial boilerplate is quite verbose, but the benefits of centering the application around an append-only log will come soon.&lt;/p&gt;

&lt;p&gt;Once a command exists, a &lt;a href="https://hexdocs.pm/commanded/commands.html#command-dispatch-and-routing"&gt;router&lt;/a&gt; is needed to determine which aggregate a command belongs to. This basic router gets the &lt;code&gt;CreateTodo&lt;/code&gt; command wired to the &lt;code&gt;Todo&lt;/code&gt; aggregate, using the &lt;code&gt;uuid&lt;/code&gt; property of the command to determine which &lt;code&gt;Todo&lt;/code&gt; instance is being referred to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;TodoBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Router&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Commanded&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Commands&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Router&lt;/span&gt;

  &lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;TodoBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Todos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Aggregates&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;
  &lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;TodoBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Todos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Commands&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;CreateTodo&lt;/span&gt;

  &lt;span class="n"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="no"&gt;CreateTodo&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;to:&lt;/span&gt; &lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;identity:&lt;/span&gt; &lt;span class="ss"&gt;:uuid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To test out the first aggregate and command, the following can be executed in an iex session (just run these commands in &lt;code&gt;iex -S mix&lt;/code&gt;)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;TodoBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;App&lt;/span&gt;
&lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;TodoBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Todos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Aggregates&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;
&lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;TodoBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Todos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Commands&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;CreateTodo&lt;/span&gt;

&lt;span class="c1"&gt;# Generate an ID for the todo item&lt;/span&gt;
&lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Ecto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Create a command instance&lt;/span&gt;
&lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;CreateTodo&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;uuid:&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;title:&lt;/span&gt; &lt;span class="s2"&gt;"Hello, world!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;completed:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;order:&lt;/span&gt; &lt;span class="mi"&gt;66&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Run the command, which is dispatched to the aggregate via the router&lt;/span&gt;
&lt;span class="no"&gt;App&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Query for the aggregate state&lt;/span&gt;
&lt;span class="no"&gt;App&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;aggregate_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Result&lt;/span&gt;
&lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;uuid:&lt;/span&gt; &lt;span class="s2"&gt;"51004ff5-5a73-4681-87bb-1b1ffbf03fe0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;title:&lt;/span&gt; &lt;span class="s2"&gt;"Hello, world!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;completed:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;order:&lt;/span&gt; &lt;span class="mi"&gt;66&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Second command: delete todo
&lt;/h3&gt;

&lt;p&gt;(&lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/488627b77d4c2b7afb45c2434759b60c8d542614"&gt;Link to relevant commit&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Since CQRS applications rely on append-only logs, there is no way events can be deleted directly. This is problematic when the requirement to delete todo items comes around.&lt;/p&gt;

&lt;p&gt;Luckily, there is a solution to this problem: a &lt;code&gt;TodoDeleted&lt;/code&gt; event. This can act as a tombstone record describing that the todo stopped existing at a certain point.&lt;/p&gt;

&lt;p&gt;With the aggregate boilerplate out of the way, this is quite easy to implement.&lt;/p&gt;

&lt;h4&gt;
  
  
  Create the command
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;TodoBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Todos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Commands&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;DeleteTodo&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;defstruct&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="ss"&gt;:uuid&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Define the event
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;TodoBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Todos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Events&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;TodoDeleted&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nv"&gt;@derive&lt;/span&gt; &lt;span class="no"&gt;Jason&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Encoder&lt;/span&gt;
  &lt;span class="k"&gt;defstruct&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="ss"&gt;:uuid&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Add the &lt;code&gt;execute&lt;/code&gt; and &lt;code&gt;apply&lt;/code&gt; methods to the aggregate
&lt;/h4&gt;

&lt;p&gt;In this case, we are turning the aggregate state into &lt;code&gt;nil&lt;/code&gt; when receiving a &lt;code&gt;TodoDeleted&lt;/code&gt; event.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(%&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;uuid:&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;DeleteTodo&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;uuid:&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;TodoDeleted&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;uuid:&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(%&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;uuid:&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;TodoDeleted&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;uuid:&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Update the router
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="no"&gt;DeleteTodo&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;to:&lt;/span&gt; &lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;identity:&lt;/span&gt; &lt;span class="ss"&gt;:uuid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, the &lt;code&gt;DeleteTodo&lt;/code&gt; command can be handled by the &lt;code&gt;App&lt;/code&gt;!&lt;/p&gt;

&lt;h3&gt;
  
  
  Many events per command: update todo
&lt;/h3&gt;

&lt;p&gt;(&lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/05b850731328251401cf2ec52a8cdf10dd99efde"&gt;Link to relevant commit&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Up to this point, there has been a one-to-one correlation between commands and events. No decisions have been made in &lt;code&gt;execute/2&lt;/code&gt; methods.&lt;/p&gt;

&lt;p&gt;One important thing about events is that they can (and should) represent something meaningful happening in the domain of the application. For example, it would be tempting to create a &lt;code&gt;TodoUpdated&lt;/code&gt; event containing &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;completed&lt;/code&gt;, and &lt;code&gt;order&lt;/code&gt; values.&lt;/p&gt;

&lt;p&gt;Imagine this todo list app becomes the product of a company, and that company has a team that wants to collect metrics on how often items are completed. An analytics pipeline might need to consume all of the &lt;code&gt;TodoUpdated&lt;/code&gt; events to determine if any of them changed the &lt;code&gt;completed&lt;/code&gt; value. This would require knowledge of the state prior to the event. "Updated" lacks domain context and doesn't have as much utility as it could have.&lt;/p&gt;

&lt;p&gt;Instead, we should choose to break down the update into many possible events:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mark an item as completed&lt;/li&gt;
&lt;li&gt;Update the title of an item&lt;/li&gt;
&lt;li&gt;Mark an item as un-completed&lt;/li&gt;
&lt;li&gt;Update the order of an item&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these events represent something meaningful happening, in the language of the domain.&lt;/p&gt;

&lt;p&gt;To implement this—while keeping the &lt;em&gt;API semantics&lt;/em&gt; as an "update"—I created a &lt;code&gt;UpdateTodo&lt;/code&gt; command that produces many events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(%&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;UpdateTodo&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;completion_command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;is_nil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
                &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;TodoCompleted&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;uuid:&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;
                &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;TodoUncompleted&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;uuid:&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;end&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;title_command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;is_nil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;TodoTitleUpdated&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;uuid:&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;title:&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;order_command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;is_nil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;TodoOrderUpdated&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;uuid:&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;order:&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;completion_command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title_command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_command&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="no"&gt;Function&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;execute/2&lt;/code&gt; method is invoked once per command, one at a time per instance. There are no data races, so we can put our business logic in this method and decide how to translate the command into events. If the todo's &lt;code&gt;completed&lt;/code&gt; value is updated, we can emit a meaningful event. If more than one field is updated, we create more than one event.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick Note&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Filtering on the &lt;a href="https://hexdocs.pm/elixir/main/Function.html#identity/1"&gt;&lt;code&gt;Function.identity/1&lt;/code&gt;&lt;/a&gt; method is a neat little trick to remove falsy (&lt;code&gt;nil&lt;/code&gt; and &lt;code&gt;false&lt;/code&gt;) entries from an enumerable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And we're back&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To update the state of the aggregate, I implemented simple &lt;code&gt;apply/2&lt;/code&gt; methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(%&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;TodoCompleted&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="ss"&gt;completed:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(%&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;TodoUncompleted&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="ss"&gt;completed:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(%&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;TodoTitleUpdated&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;title:&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="ss"&gt;title:&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(%&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;TodoOrderUpdated&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;order:&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="ss"&gt;order:&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we have the ability to articulate things that happened &lt;em&gt;in the language of the application's domain&lt;/em&gt;!&lt;/p&gt;

&lt;h3&gt;
  
  
  Projecting state into DB
&lt;/h3&gt;

&lt;p&gt;Related commits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/14a2814c2bf617d3d531647574d2639b919a7e37"&gt;Install &lt;code&gt;commanded_ecto_projections&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/c188df2691fdce24c8a0d3e2d4400b9dbf833a8d"&gt;Replace todo model with todo projection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/83228c56e1c0ffe83fb3d95a7ac1ea092b193505"&gt;Add todo projector&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So far, three commands can produce six events. There is an aggregate that tracks the events. Its state can be fetched by directly querying &lt;a href="https://hexdocs.pm/commanded/Commanded.html#aggregate_state/4"&gt;&lt;code&gt;TodoBackend.App.aggregate_state/4&lt;/code&gt;&lt;/a&gt;. This is great as a domain exploration exercise, but it isn't queryable.&lt;/p&gt;

&lt;p&gt;Enter: the read model—a representation of state, produced as a function of the event log. Unlike the original model, we &lt;em&gt;only&lt;/em&gt; can use the read model for read operations. Aside from this one restriction, we are able to use all of the functionality available in the Ecto ORM—selecting, aggregating, and even joining.&lt;/p&gt;

&lt;p&gt;The process of creating a read model from an event log is called "projecting." To simplify the creation and maintenance of projections, I opted to use the &lt;a href="https://hexdocs.pm/commanded_ecto_projections/Commanded.Projections.Ecto.html"&gt;&lt;code&gt;commanded_ecto_projections&lt;/code&gt;&lt;/a&gt; hex package. This library uses a little bit of metaprogramming magic to add the &lt;code&gt;project&lt;/code&gt; macro to a module, causing it to add operations to an &lt;a href="https://hexdocs.pm/ecto/Ecto.Multi.html"&gt;Ecto Multi&lt;/a&gt;—which eventually is executed against the application's database.&lt;/p&gt;

&lt;p&gt;One housekeeping item that I had to take care of is replacing the numeric primary key of the table with a uuid, since we won't be relying on the sequential generated identifiers within our read model. &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/c188df2691fdce24c8a0d3e2d4400b9dbf833a8d#diff-3a9445ee0a4767763dc43fcad32c91294b40b555b9a3840d0ec00578da006522"&gt;This migration is &lt;em&gt;awful&lt;/em&gt;&lt;/a&gt; as-written since it changes the schema by &lt;em&gt;dropping all of the data in the existing table&lt;/em&gt;, but it got the job done for this learning exercise.&lt;/p&gt;

&lt;p&gt;The projector for the Todo model looks like &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/83228c56e1c0ffe83fb3d95a7ac1ea092b193505#diff-667c477b155244629e3c37f1e798562904356911efc9038052d6e10bb5f42705"&gt;this&lt;/a&gt; (with some parts omitted for brevity):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;TodoBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Todos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Projectors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Commanded&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Projections&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Ecto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# Register a name for the handler's subscription in the event store&lt;/span&gt;
    &lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="s2"&gt;"Todos.Projectors.Todo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;application:&lt;/span&gt; &lt;span class="no"&gt;TodoBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;App&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# Ensure the database operation completes before allowing the command to be considered completed.&lt;/span&gt;
    &lt;span class="ss"&gt;consistency:&lt;/span&gt; &lt;span class="ss"&gt;:strong&lt;/span&gt;

  &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;(%&lt;/span&gt;&lt;span class="no"&gt;TodoCreated&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;multi&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="no"&gt;Ecto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Multi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;multi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:todo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;uuid:&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;title:&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;completed:&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;order:&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;(%&lt;/span&gt;&lt;span class="no"&gt;TodoDeleted&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;uuid:&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;multi&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="no"&gt;Ecto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Multi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;multi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:todo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;uuid:&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;(%&lt;/span&gt;&lt;span class="no"&gt;TodoCompleted&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;uuid:&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;multi&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="no"&gt;Repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;multi&lt;/span&gt;
      &lt;span class="n"&gt;todo&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Ecto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Multi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;multi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:todo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update_changeset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;completed:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;# …more project calls below&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the projector is &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/83228c56e1c0ffe83fb3d95a7ac1ea092b193505#diff-5a69b0a76cfb6d50932d5d6dfffda104a2b10b235f144acb7e3773b024500325R1"&gt;registered with the application's supervision tree&lt;/a&gt;, the database should start populating with todo items from the log events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is one of the most important properties of an event-sourced system: a read model can easily be reconstructed from the event log. Rather than performing complicated migrations and backfills, a team can produce a new database table and replay events to produce the necessary representation.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Wire into the API
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/2d959dc90f0a5a139b21d86d65728e04d45f1c95"&gt;Link to relevant commit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, it's time to connect the commands and read model up to the API!&lt;/p&gt;

&lt;p&gt;As noted earlier, Phoenix produces a module for each context of an application. This context serves as an API for any application code—API, background job, or script—to interact with the domain without needing to know the persistence implementation details.&lt;/p&gt;

&lt;p&gt;To bring it all together, the &lt;a href="https://github.com/ChristianAlexander/todo_backend_commanded/commit/2d959dc90f0a5a139b21d86d65728e04d45f1c95"&gt;commit&lt;/a&gt; for this section shows all of the changes that were required to migrate the context over to the CQRS solution.&lt;/p&gt;

&lt;p&gt;I'll only show one method here, but they're all present in the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;create_todo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt; &lt;span class="p"&gt;\\&lt;/span&gt; &lt;span class="p"&gt;%{})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;changeset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;create_todo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt; &lt;span class="p"&gt;\\&lt;/span&gt; &lt;span class="p"&gt;%{})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Ecto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="n"&gt;attrs&lt;/span&gt;
  &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;CreateTodo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;CreateTodo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assign_uuid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="ss"&gt;:ok&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="no"&gt;App&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;consistency:&lt;/span&gt; &lt;span class="ss"&gt;:strong&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_todo!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One key aspect of this invocation is the &lt;code&gt;consistency: :strong&lt;/code&gt;, which ensures that all handlers and projectors with strong consistency enabled have committed before returning. This means the read model is ready to query after &lt;code&gt;dispatch&lt;/code&gt; has completed.&lt;/p&gt;

&lt;p&gt;By changing the context implementation, the controller didn't have to be updated (aside from the omission of a deleted todo in the return value of &lt;code&gt;delete_todo&lt;/code&gt;, which I thought was a pretty bad idea to begin with) in order to start using a CQRS solution.&lt;/p&gt;

&lt;p&gt;Every change is tracked through a log of meaningful domain events. The read model can be migrated and updated as our requirements grow. New read models can be created from the same events, in case we start to have different views or different consumers. Arbitrary handlers can be written to extract the changes into some other system. Data is never &lt;em&gt;truly&lt;/em&gt; deleted, and can be resurrected for new use cases in the future (possibly the topic of a sequel to this post).&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go from here
&lt;/h2&gt;

&lt;p&gt;With this experience kicking the tires of Commanded, I'm inspired to try building something real and useful with CQRS.&lt;/p&gt;

&lt;p&gt;As for this project, I have a few ideas for improvements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Soft delete read model: Create a projection that populates a &lt;code&gt;deleted_at&lt;/code&gt; column, to show how events can be re-used to enable new use cases like restoring deleted items&lt;/li&gt;
&lt;li&gt;Validation: Determine how to apply validation to the commands, enforcing schemas and mandatory fields&lt;/li&gt;
&lt;li&gt;Error handling: Test out the rejection of commands, returning the error messages to the API's caller&lt;/li&gt;
&lt;li&gt;Taking on a meaningful domain, not just "another todo app"&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>elixir</category>
      <category>phoenix</category>
      <category>commanded</category>
      <category>cqrs</category>
    </item>
    <item>
      <title>How to Use the Firestore Export API Before it is Released</title>
      <dc:creator>Christian Alexander</dc:creator>
      <pubDate>Fri, 20 Jul 2018 23:48:46 +0000</pubDate>
      <link>https://dev.to/christianalexander/how-to-use-the-firestore-export-api-before-it-is-released-2ikd</link>
      <guid>https://dev.to/christianalexander/how-to-use-the-firestore-export-api-before-it-is-released-2ikd</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Check out &lt;a href="https://github.com/christianalexander/FirestoreRestore" rel="noopener noreferrer"&gt;FirestoreRestore&lt;/a&gt;.&lt;br&gt;
It uses a brand new API to backup and restore your Firestore database!&lt;/p&gt;
&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;There have been many requests for Google to add backup and restore functionality to their Cloud Firestore offering. Until this week, the only method for backing up a Firestore database has been to run utilities that traverse the tree as an admin client, saving each document's JSON representation on disk.&lt;/p&gt;
&lt;h2&gt;
  
  
  Discovery
&lt;/h2&gt;

&lt;p&gt;This morning, I opened the &lt;a href="https://developers.google.com/apis-explorer/#s/firestore/v1beta1/" rel="noopener noreferrer"&gt;Google APIs Explorer&lt;/a&gt; and found two new methods listed under Firestore &lt;code&gt;v1beta1&lt;/code&gt;: &lt;code&gt;exportDocuments&lt;/code&gt; and &lt;code&gt;importDocuments&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fucb125wpn6oftg3dmg5w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fucb125wpn6oftg3dmg5w.png" alt="APIs Explorer Screenshot" width="800" height="163"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Exploration
&lt;/h2&gt;

&lt;p&gt;As the API documentation states, &lt;code&gt;firestore.projects.databases.exportDocuments&lt;/code&gt; is able to export Firestore data to Google Cloud Storage.&lt;/p&gt;

&lt;p&gt;To test this out, I opened up the &lt;a href="https://console.firebase.google.com" rel="noopener noreferrer"&gt;Firebase Console&lt;/a&gt; and created a new project. In order for there to be data to back up, I created a user document from the console.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbg9iqdzon54qzar54tqs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbg9iqdzon54qzar54tqs.png" alt="Creating a document" width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the same project, I opened the &lt;a href="https://console.cloud.google.com/storage/" rel="noopener noreferrer"&gt;Google Cloud Storage Console&lt;/a&gt; and created a bucket in which to store my backups.&lt;/p&gt;

&lt;p&gt;From the &lt;a href="https://developers.google.com/apis-explorer/#s/firestore/v1beta1/firestore.projects.databases.exportDocuments" rel="noopener noreferrer"&gt;Google APIs Explorer's &lt;code&gt;exportDocuments&lt;/code&gt; page&lt;/a&gt;, I entered the Firebase project name, as well as a path to the storage bucket, and hit &lt;strong&gt;Execute&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs1fjz9tafxhy9eza0euj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs1fjz9tafxhy9eza0euj.png" alt="Execute!" width="800" height="235"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The following is the API's response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"projects/firestore-restore/databases/(default)/operations/&amp;lt;REDACTED&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"type.googleapis.com/google.firestore.admin.v1beta1.ExportDocumentsMetadata"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"startTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2018-07-18T23:20:26.535130Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"operationState"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PROCESSING"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"outputUriPrefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gs://firestore-restore/backups/2018-07-18"&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few moments later, I opened the bucket. Sure enough, the data was present!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgtkuowyvsnozor4oob89.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgtkuowyvsnozor4oob89.png" alt="Backup in the bucket" width="665" height="144"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Restore
&lt;/h2&gt;

&lt;p&gt;Now, to test the restore functionality. I deleted the contents of the Firestore database, and &lt;a href="https://developers.google.com/apis-explorer/#s/firestore/v1beta1/firestore.projects.databases.importDocuments" rel="noopener noreferrer"&gt;ran &lt;code&gt;importDocuments&lt;/code&gt;&lt;/a&gt; with the same parameters as the export request, and sure enough, the data came back!&lt;/p&gt;

&lt;h2&gt;
  
  
  Is the Backup Complete?
&lt;/h2&gt;

&lt;p&gt;Unfortunately, there is currently no "documented" way to check the status of a Firestore export in progress. Luckily, with a little URL guessing, I was able to find the endpoint for the operation resource. It was there all along; the &lt;code&gt;name&lt;/code&gt; field in the export response is the status path!&lt;/p&gt;

&lt;h2&gt;
  
  
  A Temporary Tool: &lt;a href="https://github.com/christianalexander/FirestoreRestore" rel="noopener noreferrer"&gt;FirestoreRestore&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;As I don't know when Google will officially support and build tooling for backing up and restoring Cloud Firestore databases, I developed a simple command line utility to interact with the service.&lt;/p&gt;

&lt;p&gt;It requires the creation of a &lt;a href="https://console.cloud.google.com/iam-admin/serviceaccounts/project" rel="noopener noreferrer"&gt;service account&lt;/a&gt; with the "Cloud Datastore Import Export Admin" role. With a &lt;code&gt;JSON&lt;/code&gt; key for the service account, this tool is able to perform a backup and restore.&lt;/p&gt;

&lt;p&gt;Check out &lt;a href="https://github.com/ChristianAlexander/FirestoreRestore#readme" rel="noopener noreferrer"&gt;the README&lt;/a&gt; for more info 😀&lt;/p&gt;

&lt;h2&gt;
  
  
  Speculation
&lt;/h2&gt;

&lt;p&gt;Google will likely launch backup and restore functionality within the Firebase UI. It'll probably expose the per-collection backup capability that is present in the API. It'll probably be much easier to use than some command line program you found on the internet.&lt;/p&gt;

&lt;p&gt;Until then, enjoy the CLI!&lt;/p&gt;

</description>
      <category>firestore</category>
      <category>firebase</category>
      <category>backup</category>
      <category>tool</category>
    </item>
  </channel>
</rss>
