<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Christian Alexander</title>
    <description>Christian Alexander is a software engineer in Chandler, Arizona. He specializes in distributed systems and deployment technologies.
</description>
    <link>https://christianalexander.com/</link>
    <atom:link href="https://christianalexander.com/feed.xml" rel="self" type="application/rss+xml" />
    
      <item>
        <title>Async Elixir Telemetry</title>
        <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;hr /&gt;

&lt;p&gt;One of the most fantastic abstractions used in many Elixir applications is provided by &lt;a href=&quot;https://hexdocs.pm/telemetry/readme.html&quot; target=&quot;\_blank&quot;&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=&quot;https://nodejs.org/en/learn/asynchronous-work/the-nodejs-event-emitter&quot; target=&quot;\_blank&quot;&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=&quot;https://hexdocs.pm/phoenix/telemetry.html&quot; target=&quot;\_blank&quot;&gt;Phoenix&lt;/a&gt; (the popular web framework), &lt;a href=&quot;https://hexdocs.pm/oban/Oban.Telemetry.html&quot; target=&quot;\_blank&quot;&gt;Oban&lt;/a&gt; (the popular background job framework), and &lt;a href=&quot;https://hexdocs.pm/finch/Finch.Telemetry.html&quot; target=&quot;\_blank&quot;&gt;Finch&lt;/a&gt; (the HTTP request library used by &lt;a href=&quot;https://hexdocs.pm/req/Req.html&quot; target=&quot;\_blank&quot;&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=&quot;https://hex.pm/packages/telemetry_metrics&quot; target=&quot;\_blank&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;handle_event&lt;/code&gt; callback of each handler is invoked synchronously on each &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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;!--more--&gt;

&lt;h2 id=&quot;intent-capture-product-analytics&quot;&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=&quot;https://sprinty.dev&quot; target=&quot;\_blank&quot;&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=&quot;https://hexdocs.pm/elixir/1.16.1/GenServer.html&quot; target=&quot;\_blank&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;users&lt;/code&gt; table, it’s hard for me to know how many users I have. Without an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;events&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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=&quot;https://posthog.com/&quot; target=&quot;\_blank&quot;&gt;PostHog&lt;/a&gt; (not sponsored, just a fan). They have a &lt;a href=&quot;https://posthog.com/docs/api/post-only-endpoints&quot; target=&quot;\_blank&quot;&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 id=&quot;sample-code-synchronous-approach&quot;&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 id=&quot;posthog-client-module&quot;&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;/p&gt;

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Posthog&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;kn&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Logger&lt;/span&gt;

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

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

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

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

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

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

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

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

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

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

&lt;h4 id=&quot;the-synchronous-telemetry-handler&quot;&gt;The Synchronous Telemetry Handler&lt;/h4&gt;

&lt;p&gt;This handler just passes whatever is sent under the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[:posthog, :event]&lt;/code&gt; key directly to the capture method.&lt;/p&gt;

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

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

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

&lt;p&gt;To register the handler, I just called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Points.TelemetryHandlers.SimplePosthog.attach()&lt;/code&gt; in the initialization method of my application.&lt;/p&gt;

&lt;h4 id=&quot;sending-an-event&quot;&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;/p&gt;

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

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

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

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

&lt;p&gt;The value passed as the second argument to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:telemetry.execute&lt;/code&gt; makes it to PostHog.&lt;/p&gt;

&lt;h4 id=&quot;reflecting-on-the-approach&quot;&gt;Reflecting on the Approach&lt;/h4&gt;

&lt;p&gt;The biggest problem with this naive solution is that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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 id=&quot;making-it-better-async&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;handle_event&lt;/code&gt; method is called by the process that invokes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:telemetry.execute&lt;/code&gt;. To get this event into the GenServer state, I’d have to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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=&quot;https://elixirschool.com/en/lessons/storage/ets&quot; target=&quot;\_blank&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&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;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;handle_info&lt;/code&gt; method is called with a message of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;terminate&lt;/code&gt; callback makes a final attempt to send events out. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shutdown&lt;/code&gt; value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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=&quot;https://hexdocs.pm/elixir/1.16.1/GenServer.html#c:terminate/2&quot; target=&quot;\_blank&quot;&gt;in the Elixir docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;outcome&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&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>
        <pubDate>Wed, 21 Feb 2024 23:00:00 +0000</pubDate>
        <link>https://christianalexander.com/2024/02/21/async-elixir-telemetry/</link>
        <guid isPermaLink="true">https://christianalexander.com/2024/02/21/async-elixir-telemetry/</guid>
      </item>
    
      <item>
        <title>Random Cut Forests</title>
        <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;img src=&quot;/images/2023-08-06/results.png&quot; alt=&quot;Results&quot; /&gt;&lt;/p&gt;

&lt;!--more--&gt;

&lt;p&gt;&lt;a href=&quot;https://livebook.dev/run?url=https%3A%2F%2Fgist.github.com%2FChristianAlexander%2F30174b3f0f921a41d03bb9578e6bed7b&quot; target=&quot;\_blank&quot;&gt;&lt;img src=&quot;https://livebook.dev/badge/v1/black.svg&quot; alt=&quot;Run in Livebook&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In 2016, a paper called &lt;a href=&quot;https://proceedings.mlr.press/v48/guha16.pdf&quot; target=&quot;\_blank&quot;&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=&quot;https://docs.aws.amazon.com/sagemaker/latest/dg/randomcutforest.html&quot; target=&quot;\_blank&quot;&gt;docs&lt;/a&gt;). It aims to improve upon the &lt;a href=&quot;https://cs.nju.edu.cn/zhouzh/zhouzh.files/publication/icdm08b.pdf&quot; target=&quot;\_blank&quot;&gt;Isolation Forest Algorithm&lt;/a&gt;. I could have tried my hand at isolation forests, but they’re so 2008&lt;a href=&quot;https://youtu.be/4m48GqaOz90?t=178&quot; target=&quot;\_blank&quot;&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=&quot;https://www.erlang.org/doc/tutorial/nif.html&quot; target=&quot;\_blank&quot;&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 id=&quot;how-rcf-works&quot;&gt;How RCF Works&lt;/h2&gt;

&lt;h3 id=&quot;making-a-tree&quot;&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;img src=&quot;/images/2023-08-06/starting-point.png&quot; alt=&quot;Starting point&quot; /&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;img src=&quot;/images/2023-08-06/first-cut-2d.png&quot; alt=&quot;First cut, 2d&quot; /&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;img src=&quot;/images/2023-08-06/first-cut-tree.png&quot; alt=&quot;First cut, tree&quot; /&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;img src=&quot;/images/2023-08-06/subsequent-cuts-2d.png&quot; alt=&quot;Subsequent cuts, 2d&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-08-06/subsequent-cuts-tree.png&quot; alt=&quot;Subsequent cuts, tree&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;the-forest&quot;&gt;The Forest&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-08-06/forest.png&quot; alt=&quot;A forest&quot; /&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 id=&quot;evaluation&quot;&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 id=&quot;sine-wave-anomaly-exercise&quot;&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;img src=&quot;/images/2023-08-06/fuzzy-sine-with-anomaly.png&quot; alt=&quot;Sine wave&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;shingling-the-data&quot;&gt;Shingling the Data&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/W-shingling&quot; target=&quot;\_blank&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&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;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[1, 2, 3]
[2, 3, 4]
[3, 4, 5]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 id=&quot;final-results&quot;&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;img src=&quot;/images/2023-08-06/results.png&quot; alt=&quot;Results&quot; /&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;img src=&quot;/images/2023-08-06/distribution-of-outlier-scores.png&quot; alt=&quot;Distribution of Outlier Scores&quot; /&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 id=&quot;summary&quot;&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=&quot;https://livebook.dev/run?url=https%3A%2F%2Fgist.github.com%2FChristianAlexander%2F30174b3f0f921a41d03bb9578e6bed7b&quot; target=&quot;\_blank&quot;&gt;&lt;img src=&quot;https://livebook.dev/badge/v1/black.svg&quot; alt=&quot;Run in Livebook&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;citations&quot;&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>
        <pubDate>Sun, 06 Aug 2023 23:00:00 +0000</pubDate>
        <link>https://christianalexander.com/2023/08/06/random-cut-forests-in-elixir/</link>
        <guid isPermaLink="true">https://christianalexander.com/2023/08/06/random-cut-forests-in-elixir/</guid>
      </item>
    
      <item>
        <title>Mitigating Server-Side Request Forgery</title>
        <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 id=&quot;the-attack&quot;&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;!--more--&gt;

&lt;h3 id=&quot;cloud-credential-leak&quot;&gt;Cloud credential leak&lt;/h3&gt;

&lt;p&gt;Every AWS compute resource has a special internal service, called the &lt;a href=&quot;https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html&quot; target=&quot;\_blank&quot;&gt;Instance Metadata Service (IMDS)&lt;/a&gt;. This endpoint provides network information, initialization scripts, and even &lt;a href=&quot;https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#instance-metadata-security-credentials&quot; target=&quot;\_blank&quot;&gt;temporary AWS access keys&lt;/a&gt;. All of this is conveniently hosted at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&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;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;Code&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Success&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;LastUpdated&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2012-04-26T16:39:16Z&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;Type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;AWS-HMAC&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;AccessKeyId&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ASIAIOSFODNN7EXAMPLE&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;SecretAccessKey&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;Token&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;token&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;Expiration&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2017-05-17T15:09:54Z&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 class=&quot;language-plaintext highlighter-rouge&quot;&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=&quot;https://cloud.google.com/compute/docs/metadata/overview&quot; target=&quot;\_blank&quot;&gt;Google Cloud&lt;/a&gt;, &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service?tabs=windows&quot; target=&quot;\_blank&quot;&gt;Azure&lt;/a&gt;, and &lt;a href=&quot;https://docs.digitalocean.com/products/droplets/how-to/retrieve-droplet-metadata/&quot; target=&quot;\_blank&quot;&gt;DigitalOcean&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;internal-endpoints&quot;&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 id=&quot;filesystem-access&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&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 id=&quot;mitigating-the-vulnerability&quot;&gt;Mitigating the vulnerability&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://deliver.dev&quot; target=&quot;\_blank&quot;&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 id=&quot;preventing-non-https-uris&quot;&gt;Preventing non-HTTP(S) URIs&lt;/h3&gt;

&lt;p&gt;The following example shows how the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;URI.parse/1&lt;/code&gt; &lt;a href=&quot;https://hexdocs.pm/elixir/1.12/URI.html#parse/1&quot; target=&quot;\_blank&quot;&gt;method&lt;/a&gt; breaks a URI into a struct.&lt;/p&gt;

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

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

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

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;permit_uri?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
	&lt;span class=&quot;no&quot;&gt;URI&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;scheme&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;http&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;https&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 id=&quot;preventing-the-instance-metadata-ip-address&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;169.254.169.254&lt;/code&gt;, as follows:&lt;/p&gt;

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

  &lt;span class=&quot;n&quot;&gt;parsed_uri&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;scheme&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;http&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;https&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;and&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;parsed_uri&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;host&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;169.254.169.254&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&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;/div&gt;

&lt;p&gt;If an attacker arranged for a request to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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 id=&quot;looking-up-dns-hostnames&quot;&gt;Looking up DNS hostnames&lt;/h3&gt;

&lt;p&gt;Erlang’s built-in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;charlist&lt;/code&gt; instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;binary&lt;/code&gt;. Clearly, this implementation is getting complicated.&lt;/p&gt;

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

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

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

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

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

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

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

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

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

&lt;h3 id=&quot;perform-validation-on-every-request&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;123.123.123.123&lt;/code&gt;, but update the record to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;permit_uri?/1&lt;/code&gt; (or its equivalent) for each request the server makes.&lt;/p&gt;

&lt;h2 id=&quot;using-safeurl&quot;&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=&quot;https://hexdocs.pm/safeurl/SafeURL.html&quot; target=&quot;\_blank&quot;&gt;SafeURL&lt;/a&gt; that encapsulates SSRF-protecting validation into a single module.&lt;/p&gt;

&lt;p&gt;Wherever &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SSRFProtection.permit_uri?/1&lt;/code&gt; was used, we can instead use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;GET&lt;/code&gt; requests for your application through an optional &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HTTPoison&lt;/code&gt; integration.&lt;/p&gt;

&lt;h3 id=&quot;similar-libraries&quot;&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=&quot;https://rubygems.org/gems/ssrf_filter&quot; target=&quot;\_blank&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssrf_filter&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Python: &lt;a href=&quot;https://pypi.org/project/advocate/&quot; target=&quot;\_blank&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;advocate&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;taking-it-further&quot;&gt;Taking it further&lt;/h2&gt;

&lt;h3 id=&quot;dont-follow-redirects&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;https://happy-looking-site.com&lt;/code&gt;, where it redirects to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;169.254.169.254&lt;/code&gt;. If the web application follows the redirect, the attack will succeed.&lt;/p&gt;

&lt;h3 id=&quot;security-groups&quot;&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 id=&quot;proxy&quot;&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=&quot;https://aws.amazon.com/blogs/networking-and-content-delivery/providing-controlled-internet-access-through-centralised-proxy-servers-using-aws-fargate-and-privatelink/&quot; target=&quot;\_blank&quot;&gt;Squid&lt;/a&gt;, &lt;a href=&quot;https://blog.palantir.com/using-envoy-for-egress-traffic-8524d10b5ee2&quot; target=&quot;\_blank&quot;&gt;Envoy&lt;/a&gt;, or a cloud provider &lt;a href=&quot;https://aws.amazon.com/blogs/networking-and-content-delivery/migrating-from-squid-web-proxy-to-aws-network-firewall/&quot; target=&quot;\_blank&quot;&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 id=&quot;wrapping-up&quot;&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=&quot;https://csrc.nist.gov/glossary/term/defense_in_depth&quot; target=&quot;\_blank&quot;&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>
        <pubDate>Tue, 25 Apr 2023 23:00:00 +0000</pubDate>
        <link>https://christianalexander.com/2023/04/25/ssrf/</link>
        <guid isPermaLink="true">https://christianalexander.com/2023/04/25/ssrf/</guid>
      </item>
    
      <item>
        <title>Adding soft delete to a Phoenix Commanded (CQRS) API</title>
        <description>&lt;p&gt;Part two in my series on Elixir’s Commanded library.
Part one can be found &lt;a href=&quot;https://christianalexander.com/2022/05/09/elixir-commanded/&quot; target=&quot;\_blank&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;context&quot;&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=&quot;https://hexdocs.pm/commanded_ecto_projections/Commanded.Projections.Ecto.html&quot; target=&quot;\_blank&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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=&quot;https://hexdocs.pm/ecto/getting-started.html&quot; target=&quot;\_blank&quot;&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 id=&quot;goal&quot;&gt;Goal&lt;/h2&gt;

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

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

&lt;!--more--&gt;

&lt;h2 id=&quot;updating-the-aggregate&quot;&gt;Updating the aggregate&lt;/h2&gt;

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

&lt;p&gt;The first step is to add a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deleted_at&lt;/code&gt; field to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Todo&lt;/code&gt; aggregate.&lt;/p&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; defmodule TodoBackend.Todos.Aggregates.Todo do
   defstruct [
     :uuid,
     :title,
     :completed,
&lt;span class=&quot;gd&quot;&gt;-    :order
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+    :order,
+    deleted_at: nil
&lt;/span&gt;   ]

   ...
&lt;span class=&quot;gd&quot;&gt;-  def apply(%Todo{uuid: uuid}, %TodoDeleted{uuid: uuid}) do
-    nil
-  end
&lt;/span&gt;&lt;span class=&quot;gi&quot;&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;/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 class=&quot;language-plaintext highlighter-rouge&quot;&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 id=&quot;aside-aggregates-sound-slow&quot;&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 id=&quot;lifespans&quot;&gt;Lifespans&lt;/h4&gt;

&lt;p&gt;(&lt;a href=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/8c5d60e6dabdc9b41587783f4359d2730a6199eb&quot; target=&quot;\_blank&quot;&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=&quot;https://hexdocs.pm/elixir/1.13/GenServer.html&quot; target=&quot;\_blank&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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=&quot;https://hexdocs.pm/elixir/1.13/DynamicSupervisor.html&quot; target=&quot;\_blank&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DynamicSupervisor&lt;/code&gt;&lt;/a&gt; called &lt;a href=&quot;https://github.com/commanded/commanded/blob/b497d8cfde386c272902809511f8373d03652684/lib/commanded/aggregates/supervisor.ex&quot; target=&quot;\_blank&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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=&quot;https://hexdocs.pm/commanded/1.3.1/Commanded.Aggregates.AggregateLifespan.html&quot; target=&quot;\_blank&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AggregateLifespan&lt;/code&gt;&lt;/a&gt; behaviour. The default lifespan, &lt;a href=&quot;https://hexdocs.pm/commanded/1.3.1/Commanded.Aggregates.DefaultLifespan.html#content&quot; target=&quot;\_blank&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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=&quot;https://en.wikipedia.org/wiki/Out_of_memory&quot; target=&quot;\_blank&quot;&gt;OOM&lt;/a&gt; waiting to happen.&lt;/p&gt;

&lt;p&gt;To address this, I &lt;a href=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/8c5d60e6dabdc9b41587783f4359d2730a6199eb&quot; target=&quot;\_blank&quot;&gt;implemented&lt;/a&gt; a lifespan for the Todo aggregate that keeps aggregates around for a minute—unless they receive the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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 id=&quot;snapshots&quot;&gt;Snapshots&lt;/h4&gt;

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

&lt;p&gt;There are two options per aggregate: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;snapshot_every&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;snapshot_version&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;deleted_at&lt;/code&gt; field would have required an increment of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;snapshot_version&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To learn more about aggregate performance, check out &lt;a href=&quot;https://q-the-hacker.com/post/how-we-reduced-memory-usage-in-an-application-built-with-elixir-and-commanded&quot; target=&quot;\_blank&quot;&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 id=&quot;effective-dating-the-delete-event&quot;&gt;Effective dating the delete event&lt;/h2&gt;

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

&lt;p&gt;As I noted when adding the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&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=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/8677c8603c3df38acec01ede4f51e8a7ad664d43#diff-d18cc26ef219037313f4afd1ab6ddb0dbbada23795085bf8848aac4512067980R5&quot; target=&quot;\_blank&quot;&gt;added&lt;/a&gt; a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;datetime&lt;/code&gt; field to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TodoDeleted&lt;/code&gt; event struct:&lt;/p&gt;

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

&lt;p&gt;Next, I &lt;a href=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/8677c8603c3df38acec01ede4f51e8a7ad664d43#diff-7b526edfd483136debc93c34aff9f6b4005bab384d15889ada645e3887707929R35&quot; target=&quot;\_blank&quot;&gt;populated&lt;/a&gt; this value in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;execute&lt;/code&gt; method of the todo aggregate, when the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DeleteTodo&lt;/code&gt; command received. Since &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/8677c8603c3df38acec01ede4f51e8a7ad664d43#diff-7b526edfd483136debc93c34aff9f6b4005bab384d15889ada645e3887707929R86-R87&quot; target=&quot;\_blank&quot;&gt;updated&lt;/a&gt; the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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;/p&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; defmodule TodoBackend.Todos.Aggregates.Todo do
   ...
   def execute(%Todo{uuid: uuid}, %DeleteTodo{uuid: uuid}) do
&lt;span class=&quot;gd&quot;&gt;-    %TodoDeleted{uuid: uuid}
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+    %TodoDeleted{uuid: uuid, datetime: DateTime.utc_now()}
&lt;/span&gt;   end

   ...
&lt;span class=&quot;gd&quot;&gt;-  def apply(%Todo{uuid: uuid} = todo, %TodoDeleted{uuid: uuid}) do
-    %Todo{todo | deleted_at: DateTime.utc_now()}
&lt;/span&gt;&lt;span class=&quot;gi&quot;&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;/div&gt;

&lt;h3 id=&quot;read-your-writes&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;Commanded.Serialization.JsonDecoder&lt;/code&gt; protocol must be implemented on the event struct.&lt;/p&gt;

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

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

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

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

&lt;h3 id=&quot;what-about-existing-events&quot;&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=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/8677c8603c3df38acec01ede4f51e8a7ad664d43#diff-d18cc26ef219037313f4afd1ab6ddb0dbbada23795085bf8848aac4512067980R10-R25&quot; target=&quot;\_blank&quot;&gt;modified&lt;/a&gt; the JSON decoder method—substituting a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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;/p&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; defmodule TodoBackend.Todos.Events.TodoDeleted do
   ...

   defimpl Commanded.Serialization.JsonDecoder, for: TodoDeleted do
     @doc &quot;&quot;&quot;
     Parse the datetime included in the aggregate state
     &quot;&quot;&quot;
     def decode(%TodoDeleted{} = state) do
       %TodoDeleted{datetime: datetime} = state
&lt;span class=&quot;gd&quot;&gt;-
-      {:ok, dt, _} = DateTime.from_iso8601(datetime)
-
-      %TodoDeleted{state | datetime: dt}
&lt;/span&gt;&lt;span class=&quot;gi&quot;&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;/div&gt;

&lt;h2 id=&quot;updating-the-read-model&quot;&gt;Updating the read model&lt;/h2&gt;

&lt;p&gt;(&lt;a href=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/fb148f5e740b15c19674ed5ab9a4dbe8bca3fae5&quot; target=&quot;\_blank&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&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 id=&quot;preparing-the-database&quot;&gt;Preparing the database&lt;/h3&gt;

&lt;p&gt;First, I generated a migration to add the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deleted_at&lt;/code&gt; column to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todos&lt;/code&gt; table:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;mix ecto.gen.migration add_deleted_at_to_todo
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

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

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

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

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

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

&lt;p&gt;After running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix ecto.migrate&lt;/code&gt;, the database side is ready to go.&lt;/p&gt;

&lt;h3 id=&quot;updating-the-projector&quot;&gt;Updating the projector&lt;/h3&gt;

&lt;p&gt;The projector &lt;a href=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/fb148f5e740b15c19674ed5ab9a4dbe8bca3fae5#diff-667c477b155244629e3c37f1e798562904356911efc9038052d6e10bb5f42705R27-R38&quot; target=&quot;\_blank&quot;&gt;update&lt;/a&gt; is very simple: instead of deleting the row from the table, just update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deleted_at&lt;/code&gt; column.&lt;/p&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; defmodule TodoBackend.Todos.Projectors.Todo do
   ...
&lt;span class=&quot;gd&quot;&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=&quot;gi&quot;&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;/div&gt;

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

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

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

&lt;h3 id=&quot;forcefully-re-populating-the-database&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; value provided to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;use Commanded.Projections.Ecto&lt;/code&gt; call in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;iex -S mix phx.server&lt;/code&gt;):&lt;/p&gt;

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

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

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

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

&lt;h3 id=&quot;hiding-deleted-items&quot;&gt;Hiding deleted items&lt;/h3&gt;

&lt;p&gt;Now that deleted items are present in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todos&lt;/code&gt; table, it’s important to &lt;a href=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/fb148f5e740b15c19674ed5ab9a4dbe8bca3fae5#diff-afaa01c994784ec0c0ffa28cdf2a145b7d22723c401f3cf4ba9880e0dc835fc6R25-R50&quot; target=&quot;\_blank&quot;&gt;update&lt;/a&gt; the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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;/p&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; defmodule TodoBackend.Todos do
   ...

   def list_todos do
&lt;span class=&quot;gd&quot;&gt;-    Repo.all(Todo)
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+    from(t in Todo,
+      where: is_nil(t.deleted_at)
+    )
+    |&amp;gt; Repo.all()
&lt;/span&gt;   end

   ...
&lt;span class=&quot;gd&quot;&gt;-  def get_todo!(uuid), do: Repo.get_by!(Todo, uuid: uuid)
&lt;/span&gt;&lt;span class=&quot;gi&quot;&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;/div&gt;

&lt;h3 id=&quot;where-are-we-now&quot;&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=&quot;https://www.michielrook.nl/2017/11/forget-me-please-event-sourcing-gdpr/&quot; target=&quot;\_blank&quot;&gt;here&lt;/a&gt; and &lt;a href=&quot;https://www.michielrook.nl/2017/11/event-sourcing-gdpr-follow-up/&quot; target=&quot;\_blank&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;new-feature-restore-deleted-items&quot;&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 id=&quot;implementation&quot;&gt;Implementation&lt;/h3&gt;

&lt;p&gt;(&lt;a href=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/ee10fa40052c0ab73f561bc6b1734339281820be&quot; target=&quot;\_blank&quot;&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 id=&quot;create-a-restoretodo-command&quot;&gt;&lt;a href=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/ee10fa40052c0ab73f561bc6b1734339281820be#diff-83580821993b9a15859cbea46472f80d5fb5b9edf30329066de09987e3d3a8edR1-R5&quot; target=&quot;\_blank&quot;&gt;Create&lt;/a&gt; a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RestoreTodo&lt;/code&gt; command&lt;/h4&gt;

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

&lt;h4 id=&quot;create-a-todorestored-event&quot;&gt;&lt;a href=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/ee10fa40052c0ab73f561bc6b1734339281820be#diff-3151baf357eed2a693f93a3728de3207dbf5beb9adb5a4ece13e6c50c3d77a36R1-R6&quot; target=&quot;\_blank&quot;&gt;Create&lt;/a&gt; a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TodoRestored&lt;/code&gt; event&lt;/h4&gt;

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

&lt;h4 id=&quot;update-the-aggregate&quot;&gt;Update the aggregate&lt;/h4&gt;

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

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

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

&lt;h4 id=&quot;update-the-projector&quot;&gt;&lt;a href=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/ee10fa40052c0ab73f561bc6b1734339281820be#diff-667c477b155244629e3c37f1e798562904356911efc9038052d6e10bb5f42705R42-R51&quot; target=&quot;\_blank&quot;&gt;Update&lt;/a&gt; the projector&lt;/h4&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; defmodule TodoBackend.Todos.Projectors.Todo do
   ...
&lt;span class=&quot;gi&quot;&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;/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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;RestoreTodo&lt;/code&gt; command.&lt;/p&gt;

&lt;h3 id=&quot;adding-the-api-method&quot;&gt;Adding the API method&lt;/h3&gt;

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

&lt;p&gt;To dispatch the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RestoreTodo&lt;/code&gt; command, I added &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PUT /api/todos/:id/restore&lt;/code&gt; to the API.&lt;/p&gt;

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

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;TodoBackend&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Todos&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;

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

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

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

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;TodoBackendWeb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;TodoController&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;

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

&lt;h4 id=&quot;register-the-route&quot;&gt;&lt;a href=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/3ccdff11562a20a0447769dec2355c40ee7529e9#diff-4ae4e029f3ef4d2cc48d41b90baa205d0a374da25c5ee2f2ff754de5ea726806R13&quot; target=&quot;\_blank&quot;&gt;Register&lt;/a&gt; the route&lt;/h4&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; defmodule TodoBackendWeb.Router do
   ...

   scope &quot;/api&quot;, TodoBackendWeb do
     pipe_through :api

     resources &quot;/todos&quot;, TodoController
     delete &quot;/todos&quot;, TodoController, :delete_all
&lt;span class=&quot;gi&quot;&gt;+    put &quot;/todos/:id/restore&quot;, TodoController, :restore
&lt;/span&gt;   end
 end
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;validation&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&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;/p&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; defmodule TodoBackend.Todos.Aggregates.Todo do
   ...
&lt;span class=&quot;gd&quot;&gt;-  def execute(%Todo{uuid: uuid}, %DeleteTodo{uuid: uuid}) do
&lt;/span&gt;&lt;span class=&quot;gi&quot;&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=&quot;gi&quot;&gt;+  def execute(%Todo{}, %DeleteTodo{}) do
+    {:error, &quot;Can not delete todo that is already deleted&quot;}
+  end
&lt;/span&gt; end
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This pattern-matched method will only emit a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TodoDeleted&lt;/code&gt; event when the existing aggregate state contains a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deleted_at&lt;/code&gt; value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nil&lt;/code&gt;. In all other cases, it will fall through to the implementation that returns an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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;/p&gt;

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

&lt;h2 id=&quot;summary&quot;&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=&quot;https://rubygems.org/gems/paper_trail&quot; target=&quot;\_blank&quot;&gt;paper trail&lt;/a&gt; or &lt;a href=&quot;https://django-simple-history.readthedocs.io/en/latest/&quot; target=&quot;\_blank&quot;&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>
        <pubDate>Wed, 01 Jun 2022 15:00:00 +0000</pubDate>
        <link>https://christianalexander.com/2022/06/01/elixir-commanded-soft-delete/</link>
        <guid isPermaLink="true">https://christianalexander.com/2022/06/01/elixir-commanded-soft-delete/</guid>
      </item>
    
      <item>
        <title>Using CQRS in a simple Phoenix API with Commanded</title>
        <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=&quot;https://github.com/ChristianAlexander/todo_backend_commanded&quot; target=&quot;\_blank&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todo_backend_commanded&lt;/code&gt;&lt;/a&gt;.
Its &lt;a href=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commits/FirstPost&quot; target=&quot;\_blank&quot;&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;!--more--&gt;

&lt;h2 id=&quot;context&quot;&gt;Context&lt;/h2&gt;

&lt;p&gt;I have been curious about the concepts of &lt;a href=&quot;https://docs.microsoft.com/en-us/azure/architecture/patterns/event-sourcing&quot; target=&quot;\_blank&quot;&gt;event sourcing&lt;/a&gt; and &lt;a href=&quot;https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs&quot; target=&quot;\_blank&quot;&gt;CQRS&lt;/a&gt; for a while— obsessively reading books like &lt;a href=&quot;https://www.pragprog.com/titles/egmicro/practical-microservices/&quot; target=&quot;\_blank&quot;&gt;Practical Microservices (Garofolo)&lt;/a&gt; and &lt;a href=&quot;https://www.oreilly.com/library/view/architecture-patterns-with/9781492052197/&quot; target=&quot;\_blank&quot;&gt;Architecture Patterns with Python (Percival, Gregory)&lt;/a&gt;, along with documentation for libraries like &lt;a href=&quot;https://www.sequent.io/&quot; target=&quot;\_blank&quot;&gt;Sequent (Ruby)&lt;/a&gt;, &lt;a href=&quot;https://hexdocs.pm/commanded/Commanded.html&quot; target=&quot;\_blank&quot;&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=&quot;https://kickstarter.engineering/event-sourcing-made-simple-4a2625113224&quot; target=&quot;\_blank&quot;&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 id=&quot;what-did-i-do&quot;&gt;What did I do?&lt;/h2&gt;

&lt;p&gt;As noted in my &lt;a href=&quot;/2017/09/02/trying-out-asp-dot-net-core/&quot; target=&quot;\_blank&quot;&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=&quot;https://www.todobackend.com/&quot; target=&quot;\_blank&quot;&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=&quot;https://www.phoenixframework.org/&quot; target=&quot;\_blank&quot;&gt;Phoenix Framework&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In fact, the &lt;a href=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/e58b3b5eb95dcf7302ac2d665b65855afd8d9d0b&quot; target=&quot;\_blank&quot;&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;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;mix phx.new todo_backend_commanded &lt;span class=&quot;nt&quot;&gt;--app&lt;/span&gt; todo_backend &lt;span class=&quot;nt&quot;&gt;--no-assets&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--no-html&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--no-gettext&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--no-dashboard&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--no-live&lt;/span&gt; &lt;span class=&quot;nt&quot;&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;/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=&quot;https://hexdocs.pm/ecto&quot; target=&quot;\_blank&quot;&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 id=&quot;commanded&quot;&gt;Commanded&lt;/h3&gt;

&lt;p&gt;The Commanded hex package is a fabulous CQRS library &lt;a href=&quot;https://github.com/commanded/commanded/wiki/Companies-using-Commanded&quot; target=&quot;\_blank&quot;&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=&quot;https://github.com/slashdotdash/conduit&quot; target=&quot;\_blank&quot;&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=&quot;https://www.youtube.com/watch?v=S3f6sAXa3-c&quot; target=&quot;\_blank&quot;&gt;conference talk&lt;/a&gt; from 2018.&lt;/p&gt;

&lt;h3 id=&quot;initializing&quot;&gt;Initializing&lt;/h3&gt;

&lt;p&gt;To get started with Commanded, I &lt;a href=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/c923c978108528ca0d6490c38a4a15950758b75b&quot; target=&quot;\_blank&quot;&gt;installed the hex package&lt;/a&gt; and &lt;a href=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/d9877f27ceb65371c762dbe69596edccd6fac5a5&quot; target=&quot;\_blank&quot;&gt;created two modules&lt;/a&gt; within the TodoBackend application, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;App&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EventStore&lt;/code&gt;:&lt;/p&gt;

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

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

&lt;p&gt;Both of these modules rely on macros exposed by Commanded and EventStore. The Commanded &lt;a href=&quot;https://hexdocs.pm/commanded/Commanded.Application.html&quot; target=&quot;\_blank&quot;&gt;documentation&lt;/a&gt; uses the name &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Application&lt;/code&gt;, but this didn’t make much sense to me, since TodoBackend already had an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Application&lt;/code&gt; module for the &lt;a href=&quot;https://elixir-lang.org/getting-started/mix-otp/supervisor-and-application.html&quot; target=&quot;\_blank&quot;&gt;supervision tree&lt;/a&gt;. Initially, I even thought that I was supposed to add the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Commanded.Application&lt;/code&gt; macro to the supervisor. Once that didn’t work, I renamed it to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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=&quot;https://github.com/commanded/eventstore&quot; target=&quot;\_blank&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EventStore&lt;/code&gt;&lt;/a&gt; rather than installing and babysitting EventStoreDB. To initialize the database and tables, I ran &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix event_store.init&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix event_store.create&lt;/code&gt;.&lt;/p&gt;

&lt;h3 id=&quot;first-command-create-todo&quot;&gt;First command: create todo&lt;/h3&gt;

&lt;p&gt;(&lt;a href=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/dc6e9fb7f6b704211a47819d4e9e398c4a237d98&quot; target=&quot;\_blank&quot;&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=&quot;https://martinfowler.com/bliki/DDD_Aggregate.html&quot; target=&quot;\_blank&quot;&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;/p&gt;

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;TodoBackend&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Todos&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Aggregates&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Todo&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;defstruct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;:uuid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;:title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;:completed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;:order&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;CreateTodo&lt;/code&gt;, which is defined as a struct with the same fields as the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Todo&lt;/code&gt; aggregate. I also created a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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=&quot;https://hexdocs.pm/commanded/aggregates.html#command-functions&quot; target=&quot;\_blank&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;Todo&lt;/code&gt; aggregate implementation includes the following two methods:&lt;/p&gt;

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

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(%&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Todo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;todo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;TodoCreated&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;created&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Todo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;todo&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;uuid:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;created&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;ss&quot;&gt;title:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;created&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;ss&quot;&gt;completed:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;created&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;completed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;ss&quot;&gt;order:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;created&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;order&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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=&quot;https://hexdocs.pm/commanded/commands.html#command-dispatch-and-routing&quot; target=&quot;\_blank&quot;&gt;router&lt;/a&gt; is needed to determine which aggregate a command belongs to. This basic router gets the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CreateTodo&lt;/code&gt; command wired to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Todo&lt;/code&gt; aggregate, using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uuid&lt;/code&gt; property of the command to determine which &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Todo&lt;/code&gt; instance is being referred to:&lt;/p&gt;

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

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

  &lt;span class=&quot;n&quot;&gt;dispatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;CreateTodo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;to:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Todo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;identity:&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:uuid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;iex -S mix&lt;/code&gt;)&lt;/p&gt;

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

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

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

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

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

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

&lt;h3 id=&quot;second-command-delete-todo&quot;&gt;Second command: delete todo&lt;/h3&gt;

&lt;p&gt;(&lt;a href=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/488627b77d4c2b7afb45c2434759b60c8d542614&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&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;ol&gt;
  &lt;li&gt;Create the command&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ol start=&quot;2&quot;&gt;
  &lt;li&gt;Define the event&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ol start=&quot;3&quot;&gt;
  &lt;li&gt;Add the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;execute&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;apply&lt;/code&gt; methods to the aggregate&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this case, we are turning the aggregate state into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nil&lt;/code&gt; when receiving a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TodoDeleted&lt;/code&gt; event.&lt;/p&gt;

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

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

&lt;ol start=&quot;4&quot;&gt;
  &lt;li&gt;Update the router&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Now, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DeleteTodo&lt;/code&gt; command can be handled by the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;App&lt;/code&gt;!&lt;/p&gt;

&lt;h3 id=&quot;many-events-per-command-update-todo&quot;&gt;Many events per command: update todo&lt;/h3&gt;

&lt;p&gt;(&lt;a href=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/05b850731328251401cf2ec52a8cdf10dd99efde&quot; target=&quot;\_blank&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;TodoUpdated&lt;/code&gt; event containing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;title&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;completed&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;TodoUpdated&lt;/code&gt; events to determine if any of them changed the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;UpdateTodo&lt;/code&gt; command that produces many events:&lt;/p&gt;

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

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

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

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

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&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=&quot;https://hexdocs.pm/elixir/main/Function.html#identity/1&quot; target=&quot;\_blank&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Function.identity/1&lt;/code&gt;&lt;/a&gt; method is a neat little trick to remove falsy (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nil&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;apply/2&lt;/code&gt; methods:&lt;/p&gt;

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

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

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

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(%&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Todo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;todo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;TodoOrderUpdated&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;order:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Todo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;todo&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;order:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 id=&quot;projecting-state-into-db&quot;&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=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/14a2814c2bf617d3d531647574d2639b919a7e37&quot; target=&quot;\_blank&quot;&gt;Install &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;commanded_ecto_projections&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/c188df2691fdce24c8a0d3e2d4400b9dbf833a8d&quot; target=&quot;\_blank&quot;&gt;Replace todo model with todo projection&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/83228c56e1c0ffe83fb3d95a7ac1ea092b193505&quot; target=&quot;\_blank&quot;&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=&quot;https://hexdocs.pm/commanded/Commanded.html#aggregate_state/4&quot; target=&quot;\_blank&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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=&quot;https://hexdocs.pm/commanded_ecto_projections/Commanded.Projections.Ecto.html&quot; target=&quot;\_blank&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;project&lt;/code&gt; macro to a module, causing it to add operations to an &lt;a href=&quot;https://hexdocs.pm/ecto/Ecto.Multi.html&quot; target=&quot;\_blank&quot;&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=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/c188df2691fdce24c8a0d3e2d4400b9dbf833a8d#diff-3a9445ee0a4767763dc43fcad32c91294b40b555b9a3840d0ec00578da006522&quot; target=&quot;\_blank&quot;&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=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/83228c56e1c0ffe83fb3d95a7ac1ea092b193505#diff-667c477b155244629e3c37f1e798562904356911efc9038052d6e10bb5f42705&quot; target=&quot;\_blank&quot;&gt;this&lt;/a&gt; (with some parts omitted for brevity):&lt;/p&gt;

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

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

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

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

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

&lt;p&gt;Once the projector is &lt;a href=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/83228c56e1c0ffe83fb3d95a7ac1ea092b193505#diff-5a69b0a76cfb6d50932d5d6dfffda104a2b10b235f144acb7e3773b024500325R1&quot; target=&quot;\_blank&quot;&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 id=&quot;wire-into-the-api&quot;&gt;Wire into the API&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/2d959dc90f0a5a139b21d86d65728e04d45f1c95&quot; target=&quot;\_blank&quot;&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=&quot;https://github.com/ChristianAlexander/todo_backend_commanded/commit/2d959dc90f0a5a139b21d86d65728e04d45f1c95&quot; target=&quot;\_blank&quot;&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;/p&gt;

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

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

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

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

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

&lt;p&gt;One key aspect of this invocation is the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&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 id=&quot;where-to-go-from-here&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&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>
        <pubDate>Mon, 09 May 2022 20:30:00 +0000</pubDate>
        <link>https://christianalexander.com/2022/05/09/elixir-commanded/</link>
        <guid isPermaLink="true">https://christianalexander.com/2022/05/09/elixir-commanded/</guid>
      </item>
    
      <item>
        <title>Firestore Backup and Restore</title>
        <description>&lt;h2 id=&quot;tldr&quot;&gt;TL;DR&lt;/h2&gt;

&lt;p&gt;Check out &lt;a href=&quot;https://github.com/christianalexander/FirestoreRestore&quot;&gt;FirestoreRestore&lt;/a&gt;.
It uses a brand new API to backup and restore your Firestore database!&lt;/p&gt;

&lt;h2 id=&quot;background&quot;&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 id=&quot;discovery&quot;&gt;Discovery&lt;/h2&gt;

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

&lt;p&gt;&lt;img src=&quot;/images/2018-07-18/api-explorer-import-export.png&quot; alt=&quot;APIs Explorer Screenshot&quot; /&gt;&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;exploration&quot;&gt;Exploration&lt;/h2&gt;

&lt;p&gt;As the API documentation states, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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=&quot;https://console.firebase.google.com&quot;&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;img src=&quot;/images/2018-07-18/add-firestore-data.png&quot; alt=&quot;Creating a document&quot; /&gt;&lt;/p&gt;

&lt;p&gt;In the same project, I opened the &lt;a href=&quot;https://console.cloud.google.com/storage/&quot;&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=&quot;https://developers.google.com/apis-explorer/#s/firestore/v1beta1/firestore.projects.databases.exportDocuments&quot;&gt;Google APIs Explorer’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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;img src=&quot;/images/2018-07-18/execute.png&quot; alt=&quot;Execute!&quot; /&gt;&lt;/p&gt;

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

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
 &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;projects/firestore-restore/databases/(default)/operations/&amp;lt;REDACTED&amp;gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
 &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;metadata&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;@type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;type.googleapis.com/google.firestore.admin.v1beta1.ExportDocumentsMetadata&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;startTime&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2018-07-18T23:20:26.535130Z&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;operationState&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;PROCESSING&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;outputUriPrefix&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;gs://firestore-restore/backups/2018-07-18&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
 &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;img src=&quot;/images/2018-07-18/backup-in-the-bucket.png&quot; alt=&quot;Backup in the bucket&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;restore&quot;&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=&quot;https://developers.google.com/apis-explorer/#s/firestore/v1beta1/firestore.projects.databases.importDocuments&quot;&gt;ran &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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 id=&quot;is-the-backup-complete&quot;&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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; field in the export response is the status path!&lt;/p&gt;

&lt;h2 id=&quot;a-temporary-tool-firestorerestore&quot;&gt;A Temporary Tool: &lt;a href=&quot;https://github.com/christianalexander/FirestoreRestore&quot;&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=&quot;https://console.cloud.google.com/iam-admin/serviceaccounts/project&quot;&gt;service account&lt;/a&gt; with the “Cloud Datastore Import Export Admin” role. With a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&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=&quot;https://github.com/ChristianAlexander/FirestoreRestore#readme&quot;&gt;the README&lt;/a&gt; for more info 😀&lt;/p&gt;

&lt;h2 id=&quot;speculation&quot;&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>
        <pubDate>Wed, 18 Jul 2018 20:30:00 +0000</pubDate>
        <link>https://christianalexander.com/2018/07/18/firestore-backup-restore/</link>
        <guid isPermaLink="true">https://christianalexander.com/2018/07/18/firestore-backup-restore/</guid>
      </item>
    
      <item>
        <title>Trying out ASP.NET Core 2.0</title>
        <description>&lt;h2 id=&quot;why-bother-with-c&quot;&gt;Why Bother with C#?&lt;/h2&gt;

&lt;p&gt;In my day job, I work with many APIs written in C#. Deployments involve running scripts that move DLLs to Windows servers running IIS. These servers have a large footprint and take a long time to initially provision. I think that the agility provided by being able to dockerize ASP.NET APIs could improve resource utilization, deployment times, and the ability to rapidly scale horizontally.&lt;/p&gt;

&lt;p&gt;Since &lt;a href=&quot;https://blogs.msdn.microsoft.com/dotnet/2017/08/14/announcing-net-standard-2-0/&quot;&gt;.NET Standard 2.0&lt;/a&gt; and &lt;a href=&quot;https://blogs.msdn.microsoft.com/webdev/2017/08/14/announcing-asp-net-core-2-0/&quot;&gt;ASP.NET Core 2.0&lt;/a&gt; were recently released, I decided to write a simple API using ASP.NET Core 2.0 and see if the dream of developing and deploying C# outside of Windows is finally a reality.&lt;/p&gt;

&lt;h2 id=&quot;example-project-todo-backend&quot;&gt;Example Project: Todo-Backend&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Try it live &lt;a href=&quot;https://www.todobackend.com/client/index.html?https://dotnetcore-todo-webapi.herokuapp.com/v1/todos&quot;&gt;here&lt;/a&gt;!&lt;/strong&gt; Code available &lt;a href=&quot;https://github.com/ChristianAlexander/dotnetcore-todo-webapi&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;http://todomvc.com/&quot;&gt;TodoMVC&lt;/a&gt; project exists to showcase front end technologies. It is a simple todo list application, typically using storage within the browser.&lt;/p&gt;

&lt;!--more--&gt;

&lt;p&gt;There is a backend version of the project called &lt;a href=&quot;https://www.todobackend.com/&quot;&gt;Todo-Backend&lt;/a&gt;, which provides a &lt;a href=&quot;https://www.todobackend.com/specs/index.html&quot;&gt;unit test page&lt;/a&gt; and a modified TodoMVC implementation that can target any backend that passes the tests.&lt;/p&gt;

&lt;h2 id=&quot;installing-boilerplate&quot;&gt;Installing Boilerplate&lt;/h2&gt;

&lt;p&gt;To begin, I created a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;webapi&lt;/code&gt; project.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Install &lt;a href=&quot;https://www.microsoft.com/net/core&quot;&gt;.NET Core&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Run this:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;dotnetcore-todo-webapi
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;dotnetcore-todo-webapi
dotnet new webapi
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This creates a runnable example application that doesn’t do much, but it saves the time of writing the boilerplate configuration files.&lt;/p&gt;

&lt;h2 id=&quot;modeling-the-data&quot;&gt;Modeling the Data&lt;/h2&gt;

&lt;p&gt;To allow the API to store and work with to-do items, I had to define what a to-do is, as well as what a to-do repository needs. For this, I created the &lt;a href=&quot;https://github.com/ChristianAlexander/dotnetcore-todo-webapi/tree/c588d6b3a8fe28e52277440ec6bddfa650e585ea/src/TodoWebApi/Data&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TodoWebApi.Data&lt;/code&gt;&lt;/a&gt; namespace, which contains:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;The &lt;a href=&quot;https://github.com/ChristianAlexander/dotnetcore-todo-webapi/blob/c588d6b3a8fe28e52277440ec6bddfa650e585ea/src/TodoWebApi/Data/Todo.cs&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Todo&lt;/code&gt; class&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;The &lt;a href=&quot;https://github.com/ChristianAlexander/dotnetcore-todo-webapi/blob/c588d6b3a8fe28e52277440ec6bddfa650e585ea/src/TodoWebApi/Data/ITodoRepository.cs&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ITodoRepository&lt;/code&gt; interface&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;An &lt;a href=&quot;https://github.com/ChristianAlexander/dotnetcore-todo-webapi/blob/c588d6b3a8fe28e52277440ec6bddfa650e585ea/src/TodoWebApi/Data/InMemory/TodoRepository.cs&quot;&gt;in-memory implementation of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ITodoRepository&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;writing-the-controller&quot;&gt;Writing the Controller&lt;/h2&gt;

&lt;p&gt;The &lt;a href=&quot;https://github.com/ChristianAlexander/dotnetcore-todo-webapi/blob/c588d6b3a8fe28e52277440ec6bddfa650e585ea/src/TodoWebApi/Controllers/TodosController.cs&quot;&gt;controller&lt;/a&gt; was very simple to write, with one exception.&lt;/p&gt;

&lt;p&gt;The Todo-Backend spec requires that each todo item contains a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;url&lt;/code&gt; field. Without knowledge of the base URL, this proved to be a challenge. I ended up writing two &lt;a href=&quot;https://github.com/ChristianAlexander/dotnetcore-todo-webapi/blob/c588d6b3a8fe28e52277440ec6bddfa650e585ea/src/TodoWebApi/Controllers/TodosController.cs#L70-L84&quot;&gt;mapping functions&lt;/a&gt; that use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Request&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For shipping in environments where load balancers terminate SSL/TLS, I added an environment variable that forces the returned URLs to use HTTPS. See &lt;a href=&quot;https://github.com/ChristianAlexander/dotnetcore-todo-webapi/issues/1&quot;&gt;Issue #1&lt;/a&gt; for more details.&lt;/p&gt;

&lt;h2 id=&quot;wiring-up-dependency-injection&quot;&gt;Wiring up Dependency Injection&lt;/h2&gt;

&lt;p&gt;Dependency injection is built into ASP.NET Core and was easy to wire up. To inject an implementation of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ITodoRepository&lt;/code&gt;, I just added &lt;a href=&quot;https://github.com/ChristianAlexander/dotnetcore-todo-webapi/blob/c588d6b3a8fe28e52277440ec6bddfa650e585ea/src/TodoWebApi/Startup.cs#L30-L32&quot;&gt;two lines&lt;/a&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ConfigureServices&lt;/code&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Startup.cs&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-csharp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;todoRepository&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TodoWebApi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;InMemory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;TodoRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;AddSingleton&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;typeof&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ITodoRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;todoRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ITodoRepository&lt;/code&gt; is then magically provided in the &lt;a href=&quot;https://github.com/ChristianAlexander/dotnetcore-todo-webapi/blob/c588d6b3a8fe28e52277440ec6bddfa650e585ea/src/TodoWebApi/Controllers/TodosController.cs#L15&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TodosController&lt;/code&gt; constructor&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;fixing-cors&quot;&gt;Fixing CORS&lt;/h2&gt;

&lt;p&gt;Since Todo-Backend directly calls APIs on different origins, CORS headers must be set up to allow any origin and method. They mention this in the &lt;a href=&quot;https://www.todobackend.com/contribute.html&quot;&gt;instructions&lt;/a&gt; for creating a new implementation. This is &lt;a href=&quot;https://github.com/ChristianAlexander/dotnetcore-todo-webapi/blob/c588d6b3a8fe28e52277440ec6bddfa650e585ea/src/TodoWebApi/Startup.cs#L47-L53&quot;&gt;done&lt;/a&gt; via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app.UseCors&lt;/code&gt;, which provides a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CorsPolicyBuilder&lt;/code&gt;.
CORS is checked by an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OPTIONS&lt;/code&gt; &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request&quot;&gt;preflight request&lt;/a&gt;. Since the controller does not have any handlers for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OPTIONS&lt;/code&gt;, I wrote &lt;a href=&quot;https://github.com/ChristianAlexander/dotnetcore-todo-webapi/blob/c588d6b3a8fe28e52277440ec6bddfa650e585ea/src/TodoWebApi/Startup.cs#L60-L67&quot;&gt;a middleware&lt;/a&gt; that returns an empty response for these requests:&lt;/p&gt;

&lt;div class=&quot;language-csharp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;OptionsMiddleware&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;IApplicationBuilder&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;n&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt;
	&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;WriteAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
	&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;extra-credit-remove-null-json-properties&quot;&gt;Extra Credit: Remove Null Json Properties&lt;/h2&gt;

&lt;p&gt;The default JSON serializer settings include null values, which seems unnecessary and could make responses larger.
Removing null values is not a requirement for Todo-Backend, but it &lt;em&gt;was&lt;/em&gt; really simple to configure &lt;a href=&quot;https://github.com/ChristianAlexander/dotnetcore-todo-webapi/blob/master/src/TodoWebApi/Startup.cs#L34-L36&quot;&gt;in Startup.cs&lt;/a&gt;:&lt;/p&gt;

&lt;div class=&quot;language-csharp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;AddJsonOptions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;opt&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;n&quot;&gt;opt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SerializerSettings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NullValueHandling&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Newtonsoft&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NullValueHandling&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Ignore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;dockerize&quot;&gt;Dockerize&lt;/h2&gt;

&lt;p&gt;I was able to use 17.05’s new &lt;a href=&quot;https://docs.docker.com/engine/userguide/eng-image/multistage-build/&quot;&gt;multi-stage build&lt;/a&gt; feature to prevent a bloated build output. Microsoft published the &lt;a href=&quot;https://hub.docker.com/r/microsoft/aspnetcore-build/&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;microsoft/aspnetcore-build&lt;/code&gt;&lt;/a&gt; and &lt;a href=&quot;https://hub.docker.com/r/microsoft/aspnetcore/&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;microsoft/aspnetcore&lt;/code&gt;&lt;/a&gt; base images for dockerizing ASP.NET Core applications.&lt;/p&gt;

&lt;p&gt;The only &lt;a href=&quot;https://github.com/ChristianAlexander/dotnetcore-todo-webapi/blob/c588d6b3a8fe28e52277440ec6bddfa650e585ea/Dockerfile#L5&quot;&gt;command&lt;/a&gt; that needs to be run in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-build&lt;/code&gt; container is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dotnet publish&lt;/code&gt;, which restores dependencies, builds the project, and outputs DLLs to a specified folder.&lt;/p&gt;

&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;

&lt;p&gt;The Todo-Backend project is a fun way to play with an unknown technology, and the kata-like test structure provides a nice path toward a complete implementation. However, I did find that many of the implementations do not handle &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PATCH&lt;/code&gt; requests correctly, throwing away the existing data that is not in the body as though they were simply &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PUT&lt;/code&gt; requests. This can likely be fixed by adding another test.&lt;/p&gt;

&lt;p&gt;ASP.NET Core is a wonderful step forward for developers who are familiar with C# but don’t want the overhead of Windows and IIS. I was able to develop the entire API without even touching a Windows machine. The quick start template provides a just enough boilerplate to get started, and the &lt;a href=&quot;https://docs.microsoft.com/en-us/aspnet/core/&quot;&gt;ASP.NET Core documentation&lt;/a&gt; is pretty thorough. Getting everything built and deployed was fairly straightforward. I am excited to see what improvements will be made now that ASP.NET has been open-sourced and is in the hands of the community.&lt;/p&gt;
</description>
        <pubDate>Sat, 02 Sep 2017 15:45:00 +0000</pubDate>
        <link>https://christianalexander.com/2017/09/02/trying-out-asp-dot-net-core/</link>
        <guid isPermaLink="true">https://christianalexander.com/2017/09/02/trying-out-asp-dot-net-core/</guid>
      </item>
    
      <item>
        <title>Kubernetes Autoscaling with Custom Metrics</title>
        <description>&lt;h2 id=&quot;background&quot;&gt;Background&lt;/h2&gt;

&lt;p&gt;One of the most amazing things about Kubernetes is its ability to automatically scale up and down.
In current (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;autoscaling/v1&lt;/code&gt;) releases, the only metric which could be used as a scaling target is CPU usage, as measured by &lt;a href=&quot;https://github.com/kubernetes/heapster&quot;&gt;heapster&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;With the introduction of the &lt;a href=&quot;https://github.com/kubernetes/community/blob/master/contributors/design-proposals/custom-metrics-api.md&quot;&gt;custom metrics api&lt;/a&gt;, deployments will be able to scale using one or more application-specific metrics, as well as CPU.&lt;/p&gt;

&lt;!--more--&gt;

&lt;h4 id=&quot;disclaimer&quot;&gt;Disclaimer&lt;/h4&gt;
&lt;p&gt;I spent way too much time trying to figure out how to do this today.
These are my findings, but I could not get it to work.
I look forward to being able to use this in the future when there are more examples to follow and when it is not hidden behind feature flags.&lt;/p&gt;

&lt;h2 id=&quot;exposing-metrics&quot;&gt;Exposing Metrics&lt;/h2&gt;

&lt;p&gt;A few months ago, the &lt;a href=&quot;https://github.com/kubernetes/metrics&quot;&gt;metrics&lt;/a&gt; repo was created for the purpose of storing type definitions and client code for publishing and gathering metrics.&lt;/p&gt;

&lt;p&gt;There was not a clear path forward from there, but luckily I discovered that a &lt;a href=&quot;https://github.com/DirectXMan12/custom-metrics-boilerplate&quot;&gt;boilerplate&lt;/a&gt; and a &lt;a href=&quot;https://github.com/DirectXMan12/k8s-prometheus-adapter&quot;&gt;Prometheus adapter&lt;/a&gt; have been created.&lt;/p&gt;

&lt;p&gt;The boilerplate defines a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CustomMetricsProvider&lt;/code&gt; interface &lt;a href=&quot;https://github.com/DirectXMan12/custom-metrics-boilerplate/blob/daee026337d92b4d89dfd7c90ca50b61e09d695a/pkg/provider/interfaces.go#L43-L77&quot;&gt;here&lt;/a&gt;, which looks like the following:&lt;/p&gt;

&lt;div class=&quot;language-golang highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CustomMetricsProvider&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;

	&lt;span class=&quot;n&quot;&gt;GetRootScopedMetricByName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;groupResource&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;schema&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GroupResource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;metricName&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;custom_metrics&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MetricValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

	&lt;span class=&quot;n&quot;&gt;GetRootScopedMetricBySelector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;groupResource&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;schema&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GroupResource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;selector&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Selector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;metricName&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;custom_metrics&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MetricValueList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

	&lt;span class=&quot;n&quot;&gt;GetNamespacedMetricByName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;groupResource&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;schema&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GroupResource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;metricName&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;custom_metrics&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MetricValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

	&lt;span class=&quot;n&quot;&gt;GetNamespacedMetricBySelector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;groupResource&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;schema&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GroupResource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;selector&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Selector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;metricName&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;custom_metrics&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MetricValueList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

	&lt;span class=&quot;n&quot;&gt;ListAllMetrics&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MetricInfo&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This interface is the core of what is necessary to create a custom metrics server.&lt;/p&gt;

&lt;p&gt;The boilerplate project inherits from &lt;a href=&quot;https://github.com/kubernetes/apiserver&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;k8s.io/apiserver&lt;/code&gt;&lt;/a&gt;, which creates APIs that are registered with the &lt;a href=&quot;https://github.com/kubernetes/kube-aggregator&quot;&gt;API registration API&lt;/a&gt; (because, of course that’s a thing).&lt;/p&gt;

&lt;h2 id=&quot;fun-with-flags&quot;&gt;Fun with Flags&lt;/h2&gt;

&lt;p&gt;The Horizontal Pod Autoscaler documentation &lt;a href=&quot;https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/&quot;&gt;says&lt;/a&gt; that to enable this feature, two flags must be enabled while deploying the cluster.
These flags are &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--horizontal-pod-autoscaler-use-rest-clients&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--apiserver&lt;/code&gt;.
I assume that in future k8s releases this will not be necessary, as it will become standard.&lt;/p&gt;

&lt;p&gt;The first flag &lt;a href=&quot;https://github.com/kubernetes/kubernetes/blob/aba95a169b546c28a1990b8aa93314ab11806a62/cmd/kube-controller-manager/app/autoscaling.go#L36-L39&quot;&gt;switches&lt;/a&gt; the autoscaling controller to use a REST client, rather than a client that calls heapster directly.&lt;/p&gt;

&lt;p&gt;The second flag enables &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kube-aggregator&lt;/code&gt;, which provides the previously mentioned API registration API.&lt;/p&gt;

&lt;h2 id=&quot;ship-it&quot;&gt;Ship It&lt;/h2&gt;

&lt;p&gt;From what I can tell, this is what the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HorizontalPodAutoscaler&lt;/code&gt; configuration will look like.&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;autoscaling/v2alpha1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;HorizontalPodAutoscaler&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;my-deployment&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;default&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;scaleTargetRef&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;apps/v1beta1&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Deployment&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;my-deployment&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;minReplicas&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;maxReplicas&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;10&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;metrics&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Object&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Service&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;name-of-metrics-app&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;metricName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;my-custom-metric&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;targetValue&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;9001&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
</description>
        <pubDate>Wed, 14 Jun 2017 20:30:00 +0000</pubDate>
        <link>https://christianalexander.com/kubernetes/scaling/2017/06/14/kubernetes-custom-autoscaling/</link>
        <guid isPermaLink="true">https://christianalexander.com/kubernetes/scaling/2017/06/14/kubernetes-custom-autoscaling/</guid>
      </item>
    
  </channel>
</rss>
