<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Posts on Arthur Jordão</title><link>/posts/</link><description>Recent content in Posts on Arthur Jordão</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><managingEditor>hi@arthurjordao.dev (Arthur Jordão)</managingEditor><webMaster>hi@arthurjordao.dev (Arthur Jordão)</webMaster><lastBuildDate>Tue, 07 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="/posts/index.xml" rel="self" type="application/rss+xml"/><item><title>Pragmatic Haskell: Building a Microservice Template That Actually Does Things</title><link>/posts/building-microservices-with-haskell/</link><pubDate>Tue, 07 Apr 2026 00:00:00 +0000</pubDate><author>hi@arthurjordao.dev (Arthur Jordão)</author><guid>/posts/building-microservices-with-haskell/</guid><description>&lt;p&gt;I&amp;rsquo;ve been working with Haskell for the last 4 years at &lt;strong&gt;NoRedink&lt;/strong&gt; and over those
years I&amp;rsquo;ve found myself happy working with the language. It has a
powerful type system that helps me build code that (almost) doesn&amp;rsquo;t break.&lt;/p&gt;
&lt;p&gt;But there is one thing: when it comes to personal projects I had no idea how
to start a new Haskell service from scratch. There was a lot of boilerplate
to write. At my job things are pretty straightforward as we have libraries
that accelerate our development process, and I wanted something similar! So I
had an idea: why not build my own common libraries for services?&lt;/p&gt;</description><content:encoded><![CDATA[<p>I&rsquo;ve been working with Haskell for the last 4 years at <strong>NoRedink</strong> and over those
years I&rsquo;ve found myself happy working with the language. It has a
powerful type system that helps me build code that (almost) doesn&rsquo;t break.</p>
<p>But there is one thing: when it comes to personal projects I had no idea how
to start a new Haskell service from scratch. There was a lot of boilerplate
to write. At my job things are pretty straightforward as we have libraries
that accelerate our development process, and I wanted something similar! So I
had an idea: why not build my own common libraries for services?</p>
<p>So I built it.</p>
<h2 id="the-idea">The idea</h2>
<p>I started with a list of what I wanted to build:</p>
<ul>
<li><strong>Component system</strong>. I wanted stateful components to be initialized at startup,
wired together explicitly, and allow them to be easily mocked on tests.</li>
<li><strong>Tracing</strong>. Correlation IDs that are logged from when a process starts
to all its side effects on every service it touches.</li>
<li><strong>Http server</strong>. To expose APIs with type-safe routing.</li>
<li><strong>Auth</strong>. JWT-based, with scopes baked into the type system and allow services to
checking scopes without external dependencies.</li>
<li><strong>Event system</strong>. Services shouldn&rsquo;t call each other over HTTP as it will decrease
overall system availability. Kafka keeps services decoupled and lets them evolve
independently.</li>
<li><strong>Database</strong>. Handle connection pooling, migration, repository pattern so we can
keep SQL out of our domain logic.</li>
</ul>
<h2 id="reader-over-io-rio">Reader over IO (RIO)</h2>
<p>There are a lot of ways you can handle IO in Haskell: effect system, stack monad
transformers, tagless final, etc. But the one I really enjoy working with is
RIO. RIO is reader monad (which allows you to read some state in an env),  that
also allows you to perform IO.</p>
<p>You might be wondering what this means exactly. Well, glad you asked.
The state that a reader will have on the environment will be the stateful
components of your system, such as databases, queues, HTTP and other things
that interact with the external world. While the IO will enable you to
perform side effects.</p>
<p>One example of App state is:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-haskell" data-lang="haskell"><span class="line"><span class="cl"><span class="kr">data</span> <span class="kt">App</span> <span class="ow">=</span> <span class="kt">App</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span> <span class="n">appLogFunc</span>     <span class="ow">::</span> <span class="o">!</span><span class="kt">LogFunc</span>
</span></span><span class="line"><span class="cl">  <span class="p">,</span> <span class="n">appDbPool</span>      <span class="ow">::</span> <span class="o">!</span><span class="kt">ConnectionPool</span>
</span></span><span class="line"><span class="cl">  <span class="p">,</span> <span class="n">appCorrelation</span> <span class="ow">::</span> <span class="o">!</span><span class="kt">CorrelationId</span>
</span></span><span class="line"><span class="cl">  <span class="p">,</span> <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span></code></pre></div><p>With that we can declare some typeclass to check if the env contains a
component:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-haskell" data-lang="haskell"><span class="line"><span class="cl"><span class="kr">class</span> <span class="kt">HasCorrelationId</span> <span class="n">env</span> <span class="kr">where</span>
</span></span><span class="line"><span class="cl">  <span class="n">correlationIdL</span> <span class="ow">::</span> <span class="kt">Lens&#39;</span> <span class="n">env</span> <span class="kt">CorrelationId</span>
</span></span></code></pre></div><p>When using a function that needs a component you declare on the type signature
the needed typeclasses that the env needs to conform.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-haskell" data-lang="haskell"><span class="line"><span class="cl"><span class="c1">-- You know exactly what this function needs</span>
</span></span><span class="line"><span class="cl"><span class="nf">listAccounts</span> <span class="ow">::</span> <span class="p">(</span><span class="kt">HasLogFunc</span> <span class="n">env</span><span class="p">,</span> <span class="kt">HasDB</span> <span class="n">env</span><span class="p">,</span> <span class="kt">HasCorrelationId</span> <span class="n">env</span><span class="p">)</span> <span class="ow">=&gt;</span> <span class="kt">RIO</span> <span class="n">env</span> <span class="p">[</span><span class="kt">Account</span><span class="p">]</span>
</span></span></code></pre></div><h2 id="tracing">Tracing</h2>
<p>Logs are one of the most useful ways to debug what is happening to your services
in production. With distributed systems it&rsquo;s hard to know what happens in each
part of the processing. Multiple services can handle the same Kafka message and
sometimes it&rsquo;s hard to follow.</p>
<p>With this problem in mind I find it useful to create correlation ids
which append a new segment every interaction with a different service. To
do that I made two small functions to generate CIDs.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-haskell" data-lang="haskell"><span class="line"><span class="cl"><span class="nf">generateCorrelationId</span> <span class="ow">::</span> <span class="p">(</span><span class="kt">MonadIO</span> <span class="n">m</span><span class="p">)</span> <span class="ow">=&gt;</span> <span class="n">m</span> <span class="kt">CorrelationId</span>
</span></span><span class="line"><span class="cl"><span class="nf">generateCorrelationId</span> <span class="ow">=</span> <span class="n">liftIO</span> <span class="o">$</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl">  <span class="kr">let</span> <span class="n">chars</span> <span class="ow">=</span> <span class="s">&#34;abcdefghijklmnopqrstuvwxyz0123456789&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="n">charsList</span> <span class="ow">=</span> <span class="kt">T</span><span class="o">.</span><span class="n">unpack</span> <span class="n">chars</span>
</span></span><span class="line"><span class="cl">  <span class="n">ids</span> <span class="ow">&lt;-</span> <span class="n">replicateM</span> <span class="mi">6</span> <span class="o">$</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">idx</span> <span class="ow">&lt;-</span> <span class="n">randomRIO</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">length</span> <span class="n">charsList</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">return</span> <span class="p">(</span><span class="n">charsList</span> <span class="o">!!</span> <span class="n">idx</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">return</span> <span class="o">$</span> <span class="kt">CorrelationId</span> <span class="o">$</span> <span class="kt">T</span><span class="o">.</span><span class="n">pack</span> <span class="n">ids</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">appendCorrelationId</span> <span class="ow">::</span> <span class="p">(</span><span class="kt">MonadIO</span> <span class="n">m</span><span class="p">)</span> <span class="ow">=&gt;</span> <span class="kt">CorrelationId</span> <span class="ow">-&gt;</span> <span class="n">m</span> <span class="kt">CorrelationId</span>
</span></span><span class="line"><span class="cl"><span class="nf">appendCorrelationId</span> <span class="p">(</span><span class="kt">CorrelationId</span> <span class="n">existingCid</span><span class="p">)</span> <span class="ow">=</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">newSegment</span> <span class="ow">&lt;-</span> <span class="n">generateCorrelationId</span>
</span></span><span class="line"><span class="cl">  <span class="n">return</span> <span class="o">$</span> <span class="kt">CorrelationId</span> <span class="o">$</span> <span class="n">existingCid</span> <span class="o">&lt;&gt;</span> <span class="s">&#34;.&#34;</span> <span class="o">&lt;&gt;</span> <span class="n">unCorrelationId</span> <span class="n">newSegment</span>
</span></span></code></pre></div><p>So that correlation IDs are logged in every service interaction, I also added them to the
log context in the environment. Note that <code>HasLogContext</code> is where the CID is saved.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-haskell" data-lang="haskell"><span class="line"><span class="cl"><span class="nf">formatContext</span> <span class="ow">::</span> <span class="kt">Map</span> <span class="kt">Text</span> <span class="kt">Text</span> <span class="ow">-&gt;</span> <span class="kt">Utf8Builder</span>
</span></span><span class="line"><span class="cl"><span class="nf">formatContext</span> <span class="n">ctx</span>
</span></span><span class="line"><span class="cl">  <span class="o">|</span> <span class="kt">Map</span><span class="o">.</span><span class="n">null</span> <span class="n">ctx</span> <span class="ow">=</span> <span class="n">mempty</span>
</span></span><span class="line"><span class="cl">  <span class="o">|</span> <span class="n">otherwise</span> <span class="ow">=</span> <span class="s">&#34;[&#34;</span> <span class="o">&lt;&gt;</span> <span class="n">mconcat</span> <span class="p">(</span><span class="n">map</span> <span class="n">formatEntry</span> <span class="p">(</span><span class="kt">Map</span><span class="o">.</span><span class="n">toList</span> <span class="n">ctx</span><span class="p">))</span> <span class="o">&lt;&gt;</span> <span class="s">&#34;] &#34;</span>
</span></span><span class="line"><span class="cl">  <span class="kr">where</span>
</span></span><span class="line"><span class="cl">    <span class="n">formatEntry</span> <span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span> <span class="ow">=</span> <span class="n">fromString</span> <span class="p">(</span><span class="kt">T</span><span class="o">.</span><span class="n">unpack</span> <span class="n">key</span><span class="p">)</span> <span class="o">&lt;&gt;</span> <span class="s">&#34;=&#34;</span> <span class="o">&lt;&gt;</span> <span class="n">fromString</span> <span class="p">(</span><span class="kt">T</span><span class="o">.</span><span class="n">unpack</span> <span class="n">value</span><span class="p">)</span> <span class="o">&lt;&gt;</span> <span class="s">&#34; &#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">logInfoC</span> <span class="ow">::</span> <span class="p">(</span><span class="kt">HasCallStack</span><span class="p">,</span> <span class="kt">HasLogFunc</span> <span class="n">env</span><span class="p">,</span> <span class="kt">HasLogContext</span> <span class="n">env</span><span class="p">,</span> <span class="kt">MonadReader</span> <span class="n">env</span> <span class="n">m</span><span class="p">,</span> <span class="kt">MonadIO</span> <span class="n">m</span><span class="p">)</span> <span class="ow">=&gt;</span> <span class="kt">Utf8Builder</span> <span class="ow">-&gt;</span> <span class="n">m</span> <span class="nb">()</span>
</span></span><span class="line"><span class="cl"><span class="nf">logInfoC</span> <span class="n">msg</span> <span class="ow">=</span> <span class="n">withFrozenCallStack</span> <span class="o">$</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">ctx</span> <span class="ow">&lt;-</span> <span class="n">view</span> <span class="n">logContextL</span>
</span></span><span class="line"><span class="cl">  <span class="n">logInfo</span> <span class="o">$</span> <span class="n">formatContext</span> <span class="n">ctx</span> <span class="o">&lt;&gt;</span> <span class="n">msg</span>
</span></span></code></pre></div><p>This produces a good pattern for logs as shown:</p>
<pre tabindex="0"><code>[cid=abc123.xyz789 ] --&gt; POST /register
[cid=abc123.xyz789 ] Register request for: user@example.com
[cid=abc123.xyz789 ] Issued token pair for user: 42
[cid=abc123.xyz789 ] &lt;-- 201 (12ms)

# account-service — same CID carried through Kafka headers:
[cid=abc123.xyz789.mn4kp1 ] user-registered consumed
[cid=abc123.xyz789.mn4kp1 ] Created account for user 42

# notification-service — same root CID again:
[cid=abc123.xyz789.mn4kp1.qq9r2x ] Notification dispatched to user@example.com
</code></pre><h2 id="http">HTTP</h2>
<p>For the HTTP server I chose Servant, which provides type safety on all endpoints,
route declarations, context and auth middleware.</p>
<p>I wanted transparent access to the RIO monad in handlers, so I could easily
declare a route and have all the stateful components available.</p>
<p>Routes are declared as Servant types, describing the path, auth, and response shape:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-haskell" data-lang="haskell"><span class="line"><span class="cl"><span class="nf">getAccountById</span> <span class="ow">::</span>
</span></span><span class="line"><span class="cl">  <span class="n">route</span>
</span></span><span class="line"><span class="cl">    <span class="kt">:-</span> <span class="kt">Summary</span> <span class="s">&#34;Get account by ID&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="kt">:&gt;</span> <span class="s">&#34;accounts&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="kt">:&gt;</span> <span class="kt">JWTAuth</span>
</span></span><span class="line"><span class="cl">      <span class="kt">:&gt;</span> <span class="kt">Capture</span> <span class="s">&#34;id&#34;</span> <span class="kt">Int64</span>
</span></span><span class="line"><span class="cl">      <span class="kt">:&gt;</span> <span class="kt">Get</span> <span class="kt">&#39;[JSON]</span> <span class="kt">Account</span><span class="p">,</span>
</span></span></code></pre></div><p>The <code>Domain</code> alias bundles the typeclasses a handler typically needs, keeping signatures readable:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-haskell" data-lang="haskell"><span class="line"><span class="cl"><span class="kr">type</span> <span class="kt">Domain</span> <span class="n">env</span> <span class="ow">=</span> <span class="p">(</span><span class="kt">HasLogFunc</span> <span class="n">env</span><span class="p">,</span> <span class="kt">HasLogContext</span> <span class="n">env</span><span class="p">,</span> <span class="kt">HasDB</span> <span class="n">env</span><span class="p">,</span> <span class="kt">HasCorrelationId</span> <span class="n">env</span><span class="p">)</span>
</span></span></code></pre></div><p>The handler implementation then uses <code>Domain env</code> as a single constraint to access logging, the database, and the correlation ID from the environment:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-haskell" data-lang="haskell"><span class="line"><span class="cl"><span class="nf">getAccountById</span> <span class="ow">::</span> <span class="kt">Domain</span> <span class="n">env</span> <span class="ow">=&gt;</span> <span class="kt">Int64</span> <span class="ow">-&gt;</span> <span class="kt">AccessTokenClaims</span> <span class="ow">-&gt;</span> <span class="kt">RIO</span> <span class="n">env</span> <span class="kt">Account</span>
</span></span><span class="line"><span class="cl"><span class="nf">getAccountById</span> <span class="n">accId</span> <span class="n">claims</span> <span class="ow">=</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">logInfoC</span> <span class="o">$</span> <span class="s">&#34;Getting account: &#34;</span> <span class="o">&lt;&gt;</span> <span class="n">displayShow</span> <span class="n">accId</span>
</span></span><span class="line"><span class="cl">  <span class="n">mAccount</span> <span class="ow">&lt;-</span> <span class="kt">Repo</span><span class="o">.</span><span class="n">findAccountById</span> <span class="n">accId</span>
</span></span><span class="line"><span class="cl">  <span class="kr">case</span> <span class="n">mAccount</span> <span class="kr">of</span>
</span></span><span class="line"><span class="cl">    <span class="kt">Nothing</span> <span class="ow">-&gt;</span> <span class="n">throwM</span> <span class="n">err404</span> <span class="p">{</span><span class="n">errBody</span> <span class="ow">=</span> <span class="s">&#34;Account not found&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="kt">Just</span> <span class="n">account</span> <span class="ow">-&gt;</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">authorize</span> <span class="n">claims</span> <span class="n">account</span>
</span></span><span class="line"><span class="cl">      <span class="n">return</span> <span class="n">account</span>
</span></span></code></pre></div><p>Note that we are always passing log context on the requests, but where is the cid
being generated?</p>
<p>It&rsquo;s been generated by a servant middleware that intercepts every request and make
sure that the log context is being properly populated. It first tries to get the
current cid if exists, and if it does append a new segment, otherwise just generate
a new one:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-haskell" data-lang="haskell"><span class="line"><span class="cl"><span class="nf">correlationIdMiddleware</span> <span class="ow">::</span> <span class="kt">Middleware</span>
</span></span><span class="line"><span class="cl"><span class="nf">correlationIdMiddleware</span> <span class="n">app</span> <span class="n">req</span> <span class="n">respond</span> <span class="ow">=</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl">  <span class="kr">let</span> <span class="n">maybeHeaderCid</span> <span class="ow">=</span> <span class="n">lookup</span> <span class="s">&#34;X-Correlation-Id&#34;</span> <span class="p">(</span><span class="n">requestHeaders</span> <span class="n">req</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">baseCid</span> <span class="ow">&lt;-</span> <span class="kr">case</span> <span class="n">maybeHeaderCid</span> <span class="kr">of</span>
</span></span><span class="line"><span class="cl">    <span class="kt">Just</span> <span class="n">headerVal</span>
</span></span><span class="line"><span class="cl">      <span class="o">|</span> <span class="n">not</span> <span class="p">(</span><span class="kt">BS</span><span class="o">.</span><span class="n">null</span> <span class="n">headerVal</span><span class="p">)</span> <span class="ow">-&gt;</span>
</span></span><span class="line"><span class="cl">          <span class="n">return</span> <span class="o">$</span> <span class="kt">CorrelationId</span> <span class="o">$</span> <span class="kt">TE</span><span class="o">.</span><span class="n">decodeUtf8</span> <span class="n">headerVal</span>
</span></span><span class="line"><span class="cl">    <span class="kr">_</span> <span class="ow">-&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="n">generateCorrelationId</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">cid</span> <span class="ow">&lt;-</span> <span class="n">appendCorrelationId</span> <span class="n">baseCid</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kr">let</span> <span class="n">req&#39;</span> <span class="ow">=</span> <span class="n">req</span> <span class="p">{</span><span class="kt">Wai</span><span class="o">.</span><span class="n">vault</span> <span class="ow">=</span> <span class="kt">Vault</span><span class="o">.</span><span class="n">insert</span> <span class="n">correlationIdKey</span> <span class="n">cid</span> <span class="p">(</span><span class="kt">Wai</span><span class="o">.</span><span class="n">vault</span> <span class="n">req</span><span class="p">)}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">app</span> <span class="n">req&#39;</span> <span class="o">$</span> <span class="nf">\</span><span class="n">response</span> <span class="ow">-&gt;</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl">    <span class="kr">let</span> <span class="n">cidHeader</span> <span class="ow">=</span> <span class="p">(</span><span class="s">&#34;X-Correlation-Id&#34;</span><span class="p">,</span> <span class="kt">TE</span><span class="o">.</span><span class="n">encodeUtf8</span> <span class="o">$</span> <span class="n">unCorrelationId</span> <span class="n">cid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="kr">let</span> <span class="n">response&#39;</span> <span class="ow">=</span> <span class="kt">Wai</span><span class="o">.</span><span class="n">mapResponseHeaders</span> <span class="p">(</span><span class="n">cidHeader</span> <span class="kt">:</span><span class="p">)</span> <span class="n">response</span>
</span></span><span class="line"><span class="cl">    <span class="n">respond</span> <span class="n">response&#39;</span>
</span></span></code></pre></div><h2 id="auth">Auth</h2>
<p>The previous handler also performs JWT-based authorization:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-haskell" data-lang="haskell"><span class="line"><span class="cl"><span class="nf">authorize</span> <span class="n">claims</span> <span class="n">account</span>
</span></span></code></pre></div><p>This function comes from the common auth library that is defined as such:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-haskell" data-lang="haskell"><span class="line"><span class="cl"><span class="nf">authorize</span> <span class="ow">::</span> <span class="p">(</span><span class="kt">AccessPolicy</span> <span class="n">r</span><span class="p">,</span> <span class="kt">MonadIO</span> <span class="n">m</span><span class="p">,</span> <span class="kt">MonadThrow</span> <span class="n">m</span><span class="p">)</span> <span class="ow">=&gt;</span> <span class="kt">AccessTokenClaims</span> <span class="ow">-&gt;</span> <span class="n">r</span> <span class="ow">-&gt;</span> <span class="n">m</span> <span class="nb">()</span>
</span></span><span class="line"><span class="cl"><span class="nf">authorize</span> <span class="n">claims</span> <span class="n">resource</span> <span class="ow">=</span>
</span></span><span class="line"><span class="cl">  <span class="n">unless</span> <span class="p">(</span><span class="n">canAccess</span> <span class="n">claims</span> <span class="n">resource</span><span class="p">)</span> <span class="o">$</span>
</span></span><span class="line"><span class="cl">    <span class="n">throwM</span> <span class="n">err403</span> <span class="p">{</span><span class="n">errBody</span> <span class="ow">=</span> <span class="s">&#34;Forbidden&#34;</span><span class="p">}</span>
</span></span></code></pre></div><p>So the domain entity needs to implement <code>AccessPolicy</code> to know what scopes/identity
the jwt needs to access a particular entity. This is an example where only admins
and account owners can access an entity:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-haskell" data-lang="haskell"><span class="line"><span class="cl"><span class="kr">instance</span> <span class="kt">AccessPolicy</span> <span class="kt">Account</span> <span class="kr">where</span>
</span></span><span class="line"><span class="cl">  <span class="n">canAccess</span> <span class="n">claims</span> <span class="n">account</span> <span class="ow">=</span>
</span></span><span class="line"><span class="cl">    <span class="s">&#34;admin&#34;</span> <span class="p">`</span><span class="n">elem</span><span class="p">`</span> <span class="n">atcScopes</span> <span class="n">claims</span>
</span></span><span class="line"><span class="cl">      <span class="o">||</span> <span class="p">(</span> <span class="s">&#34;read:accounts:own&#34;</span> <span class="p">`</span><span class="n">elem</span><span class="p">`</span> <span class="n">atcScopes</span> <span class="n">claims</span>
</span></span><span class="line"><span class="cl">             <span class="o">&amp;&amp;</span> <span class="s">&#34;user-&#34;</span> <span class="o">&lt;&gt;</span> <span class="n">pack</span> <span class="p">(</span><span class="n">show</span> <span class="p">(</span><span class="n">accountAuthUserId</span> <span class="n">account</span><span class="p">))</span> <span class="o">==</span> <span class="n">atcSubject</span> <span class="n">claims</span>
</span></span><span class="line"><span class="cl">         <span class="p">)</span>
</span></span></code></pre></div><p>As the token has all the information the service needs there is no dependency on
the auth service to perform this check.</p>
<p>There is a default auth service on the template with a default auth flow, which
works as the following:</p>
<pre tabindex="0"><code class="language-mermaid" data-lang="mermaid">  sequenceDiagram
      participant Client
      participant AuthService as Auth Service
      participant AccountService as Account Service
      participant DB
      participant Redis

      Client-&gt;&gt;AuthService: POST /auth/login
      AuthService-&gt;&gt;DB: Check credentials
      DB--&gt;&gt;AuthService: Result

      alt invalid credentials
          AuthService--&gt;&gt;Client: 401 Unauthorized
      else valid credentials
          AuthService--&gt;&gt;Client: 200 accessToken + refreshToken
      end

      Client-&gt;&gt;AccountService: GET /accounts with Bearer JWT
      AccountService-&gt;&gt;Redis: Is token blacklisted?
      Redis--&gt;&gt;AccountService: No
      AccountService--&gt;&gt;Client: 200 accounts

      Client-&gt;&gt;AuthService: POST /auth/logout with Bearer JWT
      AuthService-&gt;&gt;Redis: Blacklist token jti with TTL
      AuthService--&gt;&gt;Client: 200

      Client-&gt;&gt;AccountService: GET /accounts with Bearer JWT
      AccountService-&gt;&gt;Redis: Is token blacklisted?
      Redis--&gt;&gt;AccountService: Yes
      AccountService--&gt;&gt;Client: 401 Unauthorized
</code></pre><p>This is simple enough for our case and integrates well with Servant.</p>
<h2 id="event-system">Event system</h2>
<p>To increase overall availability on a distributed architecture async processing
is recommended. If one service is unavailable when another service sends a message
it won&rsquo;t affect the availability of the other service and the message will eventually
be processed.</p>
<p>For this I chose Kafka. I wanted to have the same ergonomics as HTTP requests.
Have CIDs on every message handled and propagate CIDs when needed. I won&rsquo;t show
details as it&rsquo;s very similar with the HTTP approach, but here is an example of
how a consumer works:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-haskell" data-lang="haskell"><span class="line"><span class="cl"><span class="nf">notificationsTopic</span> <span class="ow">::</span> <span class="kt">TopicName</span>
</span></span><span class="line"><span class="cl"><span class="nf">notificationsTopic</span> <span class="ow">=</span> <span class="kt">TopicName</span> <span class="s">&#34;notifications&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">-- | Build the Kafka consumer configuration.</span>
</span></span><span class="line"><span class="cl"><span class="c1">-- The consumer loop automatically sends failing messages to the dead-letter</span>
</span></span><span class="line"><span class="cl"><span class="c1">-- topic after &#39;maxRetries&#39; attempts — handlers only need to throw on error.</span>
</span></span><span class="line"><span class="cl"><span class="nf">consumerConfig</span> <span class="ow">::</span>
</span></span><span class="line"><span class="cl">  <span class="p">(</span><span class="kt">Domain</span> <span class="n">env</span><span class="p">,</span> <span class="kt">HasMetrics</span> <span class="n">env</span><span class="p">)</span> <span class="ow">=&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="kt">Settings</span> <span class="ow">-&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="kt">ConsumerConfig</span> <span class="n">env</span>
</span></span><span class="line"><span class="cl"><span class="nf">consumerConfig</span> <span class="n">kafkaSettings</span> <span class="ow">=</span>
</span></span><span class="line"><span class="cl">  <span class="kt">ConsumerConfig</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span> <span class="n">brokerAddress</span> <span class="ow">=</span> <span class="n">kafkaBroker</span> <span class="n">kafkaSettings</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">groupId</span> <span class="ow">=</span> <span class="n">kafkaGroupId</span> <span class="n">kafkaSettings</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">topicHandlers</span> <span class="ow">=</span>
</span></span><span class="line"><span class="cl">        <span class="p">[</span> <span class="kt">TopicHandler</span>
</span></span><span class="line"><span class="cl">            <span class="p">{</span> <span class="n">topic</span> <span class="ow">=</span> <span class="n">notificationsTopic</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">              <span class="n">handler</span> <span class="ow">=</span> <span class="n">notificationHandler</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">],</span>
</span></span><span class="line"><span class="cl">      <span class="n">deadLetterTopic</span> <span class="ow">=</span> <span class="kt">TopicName</span> <span class="p">(</span><span class="n">kafkaDeadLetterTopic</span> <span class="n">kafkaSettings</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="n">maxRetries</span> <span class="ow">=</span> <span class="n">kafkaMaxRetries</span> <span class="n">kafkaSettings</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">consumerRecordMessageMetrics</span> <span class="ow">=</span> <span class="n">recordKafkaMetricsInternal</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">consumerRecordOffsetMetrics</span> <span class="ow">=</span> <span class="n">recordKafkaOffsetMetricsInternal</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">-- | Parse and dispatch a notification message.</span>
</span></span><span class="line"><span class="cl"><span class="c1">-- Throws (→ dead letter) if the payload cannot be decoded as JSON.</span>
</span></span><span class="line"><span class="cl"><span class="nf">notificationHandler</span> <span class="ow">::</span>
</span></span><span class="line"><span class="cl">  <span class="kt">Domain</span> <span class="n">env</span> <span class="ow">=&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="kt">Value</span> <span class="ow">-&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="kt">RIO</span> <span class="n">env</span> <span class="nb">()</span>
</span></span><span class="line"><span class="cl"><span class="nf">notificationHandler</span> <span class="n">jsonValue</span> <span class="ow">=</span>
</span></span><span class="line"><span class="cl">  <span class="kr">case</span> <span class="kt">Aeson</span><span class="o">.</span><span class="n">fromJSON</span> <span class="o">@</span><span class="kt">NotificationMessage</span> <span class="n">jsonValue</span> <span class="kr">of</span>
</span></span><span class="line"><span class="cl">    <span class="kt">Aeson</span><span class="o">.</span><span class="kt">Error</span> <span class="n">err</span> <span class="ow">-&gt;</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl">      <span class="kr">let</span> <span class="n">msg</span> <span class="ow">=</span> <span class="s">&#34;Failed to parse notification message: &#34;</span> <span class="o">&lt;&gt;</span> <span class="n">err</span>
</span></span><span class="line"><span class="cl">      <span class="n">logErrorC</span> <span class="p">(</span><span class="n">fromString</span> <span class="n">msg</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">throwString</span> <span class="n">msg</span>
</span></span><span class="line"><span class="cl">    <span class="kt">Aeson</span><span class="o">.</span><span class="kt">Success</span> <span class="n">msg</span> <span class="ow">-&gt;</span> <span class="n">processNotification</span> <span class="n">msg</span>
</span></span></code></pre></div><p>On topic handlers we declare which topics the service is consuming and the handler.
The handler also has access to the env using the RIO monad and process the message.
In case of failure processing the message after a set number of retries, the message
is sent to a DLQ topic, which is consumed by the DLQ service.</p>
<p>The DLQ service saves the full message details to a database and exposes them
through a UI so a developer can inspect the message and choose if it should be
reprocessed or dropped.</p>
<h2 id="database">Database</h2>
<p>On the database side it uses <code>persistent</code> to handle SQL entities. This is a simple
ORM-style library so we don&rsquo;t need to handle SQL manually, but I wanted some extra tooling for debugging. One thing I find useful is to know which correlation id performed
an action so we can easily find the logs related to that entity.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-haskell" data-lang="haskell"><span class="line"><span class="cl"><span class="nf">share</span>
</span></span><span class="line"><span class="cl">  <span class="p">[</span><span class="n">mkPersist</span> <span class="n">sqlSettings</span><span class="p">,</span> <span class="n">mkMigrate</span> <span class="s">&#34;migrateAll&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">[</span><span class="n">persistWithMeta</span><span class="o">|</span>
</span></span><span class="line"><span class="cl"><span class="kt">SentNotification</span>
</span></span><span class="line"><span class="cl">  <span class="n">templateName</span> <span class="kt">Text</span>
</span></span><span class="line"><span class="cl">  <span class="n">channelType</span>  <span class="kt">Text</span>
</span></span><span class="line"><span class="cl">  <span class="n">recipient</span>    <span class="kt">Text</span>
</span></span><span class="line"><span class="cl">  <span class="n">content</span>      <span class="kt">Text</span>
</span></span><span class="line"><span class="cl">  <span class="kr">deriving</span> <span class="kt">Show</span>
</span></span><span class="line"><span class="cl">  <span class="o">|</span><span class="p">]</span>
</span></span></code></pre></div><p>This is an entity declaration for SentNotification on the notification service.
Note that this uses <code>persistWithMeta</code>. The implementation for this is a little tricky,
but in summary, this adds a cid column to the SQL table, and when saving a record
it pulls the CID from the current env automatically.</p>
<h2 id="misc">Misc</h2>
<p>Some topics that I won&rsquo;t cover here in details, but were thought during development:</p>
<h3 id="architecture-ports-and-adapters">Architecture: ports-and-adapters</h3>
<p>All services follow the same structure, organized around the domain:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">❯ tree
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">├── src
</span></span><span class="line"><span class="cl">│   ├── App.hs
</span></span><span class="line"><span class="cl">│   ├── DB
</span></span><span class="line"><span class="cl">│   │   └── Account.hs
</span></span><span class="line"><span class="cl">│   ├── Domain
</span></span><span class="line"><span class="cl">│   │   └── Accounts.hs
</span></span><span class="line"><span class="cl">│   ├── Lib.hs
</span></span><span class="line"><span class="cl">│   ├── Ports
</span></span><span class="line"><span class="cl">│   │   ├── Consumer.hs
</span></span><span class="line"><span class="cl">│   │   ├── HttpClient.hs
</span></span><span class="line"><span class="cl">│   │   ├── Produce.hs
</span></span><span class="line"><span class="cl">│   │   ├── Repository.hs
</span></span><span class="line"><span class="cl">│   │   └── Server.hs
</span></span><span class="line"><span class="cl">│   ├── Settings.hs
</span></span><span class="line"><span class="cl">│   └── Types
</span></span><span class="line"><span class="cl">│       ├── In
</span></span><span class="line"><span class="cl">│       │   └── UserRegistered.hs
</span></span><span class="line"><span class="cl">│       └── Out
</span></span><span class="line"><span class="cl">│           ├── AccountCreated.hs
</span></span><span class="line"><span class="cl">│           └── Notifications.hs
</span></span><span class="line"><span class="cl">└── <span class="nb">test</span>
</span></span><span class="line"><span class="cl">    └── Spec.hs
</span></span></code></pre></div><ul>
<li><strong><code>Domain</code></strong>: where the usecases live.</li>
<li><strong><code>Ports</code></strong>: where interactions with the external world live. The usecase calls them using domain model entities and the ports adapt them to external HTTP requests, Kafka messages, etc.</li>
<li><strong><code>Types</code></strong>: where we declare records used to communicate with other services.</li>
<li><strong><code>DB</code></strong>: where we declare database entities using the <code>persistent</code> library.</li>
<li><strong><code>Settings</code></strong>: where configuration lives: HTTP, Kafka, etc.</li>
</ul>
<p>One example is:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-haskell" data-lang="haskell"><span class="line"><span class="cl"><span class="nf">getAccount</span> <span class="ow">::</span> <span class="kt">Domain</span> <span class="n">env</span> <span class="ow">=&gt;</span> <span class="kt">Int64</span> <span class="ow">-&gt;</span> <span class="kt">AccessTokenClaims</span> <span class="ow">-&gt;</span> <span class="kt">RIO</span> <span class="n">env</span> <span class="kt">Account</span>
</span></span><span class="line"><span class="cl"><span class="nf">getAccount</span> <span class="n">accId</span> <span class="n">claims</span> <span class="ow">=</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">logInfoC</span> <span class="o">$</span> <span class="s">&#34;Getting account: &#34;</span> <span class="o">&lt;&gt;</span> <span class="n">displayShow</span> <span class="n">accId</span>
</span></span><span class="line"><span class="cl">  <span class="n">mAccount</span> <span class="ow">&lt;-</span> <span class="kt">Repo</span><span class="o">.</span><span class="n">findAccountById</span> <span class="n">accId</span>
</span></span><span class="line"><span class="cl">  <span class="kr">case</span> <span class="n">mAccount</span> <span class="kr">of</span>
</span></span><span class="line"><span class="cl">    <span class="kt">Nothing</span> <span class="ow">-&gt;</span> <span class="n">throwM</span> <span class="n">err404</span> <span class="p">{</span><span class="n">errBody</span> <span class="ow">=</span> <span class="s">&#34;Account not found&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="kt">Just</span> <span class="n">account</span> <span class="ow">-&gt;</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">authorize</span> <span class="n">claims</span> <span class="n">account</span>
</span></span><span class="line"><span class="cl">      <span class="n">return</span> <span class="n">account</span>
</span></span></code></pre></div><p>Via <code>Repo.findAccountById</code>, the domain delegates the DB query to the repository
port, which handles the SQL query. The domain doesn&rsquo;t need to care about
implementation details of the database, which are encapsulated by:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-haskell" data-lang="haskell"><span class="line"><span class="cl"><span class="nf">findAccountById</span> <span class="ow">::</span> <span class="kt">Repo</span> <span class="n">env</span> <span class="ow">=&gt;</span> <span class="kt">Int64</span> <span class="ow">-&gt;</span> <span class="kt">RIO</span> <span class="n">env</span> <span class="p">(</span><span class="kt">Maybe</span> <span class="kt">Account</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">findAccountById</span> <span class="n">accId</span> <span class="ow">=</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">pool</span> <span class="ow">&lt;-</span> <span class="n">view</span> <span class="n">dbL</span>
</span></span><span class="line"><span class="cl">  <span class="n">runSqlPoolWithCid</span> <span class="p">(</span><span class="n">get</span> <span class="p">(</span><span class="n">toSqlKey</span> <span class="n">accId</span> <span class="ow">::</span> <span class="kt">AccountId</span><span class="p">))</span> <span class="n">pool</span>
</span></span></code></pre></div><h3 id="monitoring">Monitoring</h3>
<p>I set up Grafana/Loki/Prometheus to have service metrics and logs visualization,
this makes it easy to debug any problems I could have in production.</p>
<p>One of the most useful dashboards I made was the CID investigator, which is a query
that lets you trace every part of a request since the initial call.</p>
<p><img loading="lazy" src="/posts/building-microservices-with-haskell/grafana-logs.webp" type="" alt="Grafana logs dashboard showing correlated log lines across services filtered by a single correlation ID"  /></p>
<h3 id="testing">Testing</h3>
<p>The RIO pattern allows you to mock every component of the App, so if your function
depends on a <code>HasDB env</code>, you can build an APP that implements this and test your
function.</p>
<p>Every service follows the same pattern to create a test app, which mocks
external component dependencies:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-haskell" data-lang="haskell"><span class="line"><span class="cl"><span class="nf">withTestApp</span> <span class="ow">::</span> <span class="p">(</span><span class="kt">Int</span> <span class="ow">-&gt;</span> <span class="kt">TestApp</span> <span class="ow">-&gt;</span> <span class="kt">IO</span> <span class="nb">()</span><span class="p">)</span> <span class="ow">-&gt;</span> <span class="kt">IO</span> <span class="nb">()</span>
</span></span><span class="line"><span class="cl"><span class="nf">withTestApp</span> <span class="n">action</span> <span class="ow">=</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="kr">let</span> <span class="n">testApp</span> <span class="ow">=</span>
</span></span><span class="line"><span class="cl">          <span class="kt">TestApp</span>
</span></span><span class="line"><span class="cl">            <span class="p">{</span> <span class="n">testAppLogFunc</span> <span class="ow">=</span> <span class="n">logFunc</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">              <span class="n">testAppLogContext</span> <span class="ow">=</span> <span class="kt">Map</span><span class="o">.</span><span class="n">empty</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">              <span class="n">testAppSettings</span> <span class="ow">=</span> <span class="n">testSettings</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">              <span class="n">testAppCorrelationId</span> <span class="ow">=</span> <span class="n">defaultCorrelationId</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">              <span class="n">testAppDb</span> <span class="ow">=</span> <span class="n">pool</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">              <span class="n">testAppMockKafka</span> <span class="ow">=</span> <span class="n">mockKafkaState</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">              <span class="n">testAppHttpClient</span> <span class="ow">=</span> <span class="n">httpClient</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">              <span class="n">testAppMockHttp</span> <span class="ow">=</span> <span class="n">mockHttpState</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">              <span class="n">testAppMetrics</span> <span class="ow">=</span> <span class="n">metrics</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">liftIO</span> <span class="o">$</span> <span class="n">testWithApplication</span> <span class="p">(</span><span class="n">pure</span> <span class="o">$</span> <span class="n">testAppToWai</span> <span class="n">testApp</span><span class="p">)</span> <span class="o">$</span> <span class="nf">\</span><span class="n">port&#39;</span> <span class="ow">-&gt;</span> <span class="n">action</span> <span class="n">port&#39;</span> <span class="n">testApp</span>
</span></span></code></pre></div><p>With that we are able to make some tests like this http test:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-haskell" data-lang="haskell"><span class="line"><span class="cl"><span class="nf">it</span> <span class="s">&#34;respond with 200 on status&#34;</span> <span class="o">$</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">withTestApp</span> <span class="o">$</span> <span class="nf">\</span><span class="n">port&#39;</span> <span class="kr">_</span> <span class="ow">-&gt;</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">manager</span> <span class="ow">&lt;-</span> <span class="n">newManager</span> <span class="n">defaultManagerSettings</span>
</span></span><span class="line"><span class="cl">    <span class="n">request</span> <span class="ow">&lt;-</span> <span class="n">parseRequest</span> <span class="p">(</span><span class="n">baseUrl</span> <span class="n">port&#39;</span> <span class="o">&lt;&gt;</span> <span class="s">&#34;/status&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">response</span> <span class="ow">&lt;-</span> <span class="n">httpLbs</span> <span class="n">request</span> <span class="n">manager</span>
</span></span><span class="line"><span class="cl">    <span class="n">responseStatus</span> <span class="n">response</span> <span class="p">`</span><span class="n">shouldBe</span><span class="p">`</span> <span class="n">status200</span>
</span></span><span class="line"><span class="cl">    <span class="n">responseBody</span> <span class="n">response</span> <span class="p">`</span><span class="n">shouldBe</span><span class="p">`</span> <span class="s">&#34;</span><span class="se">\&#34;</span><span class="s">OK</span><span class="se">\&#34;</span><span class="s">&#34;</span>
</span></span></code></pre></div><p>Since <code>withTestApp</code> populates the <code>testApp</code>, it can also produce Kafka messages, for example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-haskell" data-lang="haskell"><span class="line"><span class="cl"><span class="nf">setupAccount</span> <span class="ow">::</span> <span class="kt">TestApp</span> <span class="ow">-&gt;</span> <span class="kt">Int64</span> <span class="ow">-&gt;</span> <span class="kt">Text</span> <span class="ow">-&gt;</span> <span class="kt">IO</span> <span class="nb">()</span>
</span></span><span class="line"><span class="cl"><span class="nf">setupAccount</span> <span class="n">testApp</span> <span class="n">uid</span> <span class="n">email</span> <span class="ow">=</span> <span class="n">runRIO</span> <span class="n">testApp</span> <span class="o">$</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl">  <span class="kr">let</span> <span class="n">mockKafka</span> <span class="ow">=</span> <span class="n">testAppMockKafka</span> <span class="n">testApp</span>
</span></span><span class="line"><span class="cl">      <span class="n">event</span> <span class="ow">=</span> <span class="kt">UserRegisteredEvent</span> <span class="p">{</span><span class="n">userId</span> <span class="ow">=</span> <span class="n">uid</span><span class="p">,</span> <span class="n">email</span> <span class="ow">=</span> <span class="n">email</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="n">consumerCfg</span> <span class="ow">=</span> <span class="kt">KafkaPort</span><span class="o">.</span><span class="n">consumerConfig</span> <span class="p">(</span><span class="kt">Settings</span><span class="o">.</span><span class="n">kafka</span> <span class="p">(</span><span class="n">testAppSettings</span> <span class="n">testApp</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">  <span class="n">mockProduceMessage</span> <span class="p">(</span><span class="kt">MockProducer</span> <span class="n">mockKafka</span><span class="p">)</span> <span class="p">(</span><span class="kt">TopicName</span> <span class="s">&#34;user-registered&#34;</span><span class="p">)</span> <span class="kt">Nothing</span> <span class="n">event</span>
</span></span><span class="line"><span class="cl">  <span class="n">processAllMessages</span> <span class="p">(</span><span class="kt">MockConsumer</span> <span class="n">mockKafka</span><span class="p">)</span> <span class="n">consumerCfg</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="nf">it</span> <span class="s">&#34;creates account automatically on user-registered Kafka event&#34;</span> <span class="o">$</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">withTestApp</span> <span class="o">$</span> <span class="nf">\</span><span class="n">port&#39;</span> <span class="n">testApp</span> <span class="ow">-&gt;</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">setupAccount</span> <span class="n">testApp</span> <span class="mi">42</span> <span class="s">&#34;eve@example.com&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">-- Account is the first inserted row so its DB primary key is 1.</span>
</span></span><span class="line"><span class="cl">    <span class="c1">-- The bearer token carries the auth user ID (42), which must match account.authUserId.</span>
</span></span><span class="line"><span class="cl">    <span class="n">manager</span> <span class="ow">&lt;-</span> <span class="n">newManager</span> <span class="n">defaultManagerSettings</span>
</span></span><span class="line"><span class="cl">    <span class="n">req</span> <span class="ow">&lt;-</span> <span class="n">parseRequest</span> <span class="p">(</span><span class="n">baseUrl</span> <span class="n">port&#39;</span> <span class="o">&lt;&gt;</span> <span class="s">&#34;/accounts/1&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">resp</span> <span class="ow">&lt;-</span>
</span></span><span class="line"><span class="cl">      <span class="n">httpLbs</span>
</span></span><span class="line"><span class="cl">        <span class="n">req</span> <span class="p">{</span><span class="n">requestHeaders</span> <span class="ow">=</span> <span class="p">[(</span><span class="s">&#34;Authorization&#34;</span><span class="p">,</span> <span class="s">&#34;Bearer token-user-42&#34;</span><span class="p">)]}</span>
</span></span><span class="line"><span class="cl">        <span class="n">manager</span>
</span></span><span class="line"><span class="cl">    <span class="n">statusCode</span> <span class="p">(</span><span class="n">responseStatus</span> <span class="n">resp</span><span class="p">)</span> <span class="p">`</span><span class="n">shouldBe</span><span class="p">`</span> <span class="mi">200</span>
</span></span></code></pre></div><h3 id="admin-ui">Admin UI</h3>
<p>I also built a small react UI to have some admin capabilities.</p>
<p>It has:</p>
<ul>
<li>login, so only admins can access it.</li>
<li>Deadletter queue management, so I can inspect and replay messages that failed during async processing.</li>
<li>Notifications that got sent to other users.</li>
</ul>
<p><img loading="lazy" src="/posts/building-microservices-with-haskell/dlq-admin.webp" type="" alt="Dead letter queue management page showing failed messages with their topic, error and retry count"  /></p>
<p><img loading="lazy" src="/posts/building-microservices-with-haskell/notification-admin.webp" type="" alt="Notifications page showing sent notifications with recipient and rendered template content"  /></p>
<p><img loading="lazy" src="/posts/building-microservices-with-haskell/users-admin.webp" type="" alt="Users page showing registered users"  /></p>
<h2 id="how-was-the-experience-so-far">How was the experience so far?</h2>
<p>Building this tooling was very satisfying for myself. I find that Haskell has
a unique superpower: it gives you confidence on the type system so you know that
what you built is probably going to work.</p>
<p>Sadly, Haskell tooling is limited, there isn&rsquo;t a ton of Haskell libraries as there are
in more popular languages such as JavaScript. Type errors can be tricky to figure
out. Have that in mind if you want to explore Haskell.</p>
<p>I am also sharing the code of this adventure as a reference, this is not a production
ready code as it was mostly a project for fun, but there is a lot to learn from
this.</p>
<p><em>Code: <a href="https://github.com/arthurjordao/haskell-service-template">github.com/arthurjordao/haskell-service-template</a></em></p>
]]></content:encoded></item><item><title>Tmux + Tmuxinator + FZF workflow</title><link>/posts/tmux-tmuxinator-fzf-workflow/</link><pubDate>Tue, 12 Sep 2023 01:00:00 +0000</pubDate><author>hi@arthurjordao.dev (Arthur Jordão)</author><guid>/posts/tmux-tmuxinator-fzf-workflow/</guid><description>An adventure for the perfect tmux workflow.</description><content:encoded><![CDATA[<p>I adopted tmux to my workflow recently and one of the things that I wanted on my workflow was the ability to create tmux sessions for projects that I was working on using a specific layout. Most often I found myself using the following layout:</p>
<ul>
<li>Window 1 (editor)
<ul>
<li>nvim .</li>
</ul>
</li>
<li>Window 2 (workspace vertical)
<ul>
<li>Server</li>
<li>Random commands workspace.</li>
</ul>
</li>
</ul>
<p>Creating this tmux session was kind of boring. My flow was something like:</p>
<ul>
<li><code>cd</code> to the project.</li>
<li>Start <code>nvim</code> on the first window.</li>
<li>Create a new panel</li>
<li>Split the window into two panels.</li>
</ul>
<p>I thought well, I can optimize that!</p>
<p>First I tried <a href="https://github.com/tmuxinator/tmuxinator#erb">tmuxinator</a>, which is an awesome tool that you can describe a tmux session using yaml. You just need to save a yaml file under <code>.config/tmuxinator/sample.yml</code>, and the configuration looks something like that:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># ~/.config/tmuxinator/sample.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">sample</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">root</span><span class="p">:</span><span class="w"> </span><span class="l">~/dev/personal/sample</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">windows</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">editor</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">layout</span><span class="p">:</span><span class="w"> </span><span class="l">main-vertical</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">panes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="l">vim</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">server</span><span class="p">:</span><span class="w"> </span><span class="l">bundle exec rails s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">logs</span><span class="p">:</span><span class="w"> </span><span class="l">tail -f log/development.log</span><span class="w">
</span></span></span></code></pre></div><p>I started to use it, and I could just execute <code>tmuxinator start sample</code> and everything was setup as I wanted, but one thing that bored me was creating one tmuxinator config per project, and also having to type all the tmuxinator command, needing to remember the name of the project that I wanted to work on <del>my memory sucks!</del>.</p>
<p>So, I got inspiration from <a href="https://github.com/ThePrimeagen/.dotfiles/blob/master/bin/.local/scripts/tmux-sessionizer">tmux sessionizer</a> and gave a try to a way to integrate my tmux workflow with <code>fzf</code>. So I created a base <code>tmuxinator</code> configuration, which is the following:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># ~/.tmuxinator/project.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">project</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">root</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;%= @settings[&#34;workspace&#34;] %&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">windows</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">editor</span><span class="p">:</span><span class="w"> </span><span class="l">nvim .</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">server</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">layout</span><span class="p">:</span><span class="w"> </span><span class="l">main-vertical</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">panes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>-<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>-<span class="w">
</span></span></span></code></pre></div><p>In this configuration, I just have my default config for every project which is one window with the editor and another with server/workspace panels. This config works for me in 90% of the projects.</p>
<p>Note that on the root I added an <a href="https://github.com/tmuxinator/tmuxinator#erb">erb</a> syntax for passing the project directory.</p>
<p>Using this configuration, I created a new <code>bash</code> command using <code>fzf</code>, that finds all the folders located under <code>~/dev/noredink</code> (my work dev directory), and <code>~/dev/personal</code> (my personal dev directory). After that, I piped it to <code>fzf</code> so I could select the project that I wanted to work on.</p>
<p>If in the folder there is a <code>.tmuxinator.yml</code> I use the local config for the project, if not I use the default config and then I create a new tmux session using tmuxinator.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># ~/.local/bin</span>
</span></span><span class="line"><span class="cl"><span class="c1">#!/usr/bin/env bash</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="nv">$#</span> -eq <span class="m">1</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nv">selected</span><span class="o">=</span><span class="nv">$1</span>
</span></span><span class="line"><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="cl">    <span class="nv">selected</span><span class="o">=</span><span class="k">$(</span>find ~/dev/noredink ~/dev/personal -mindepth <span class="m">1</span> -maxdepth <span class="m">1</span> -type d <span class="p">|</span> fzf<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> -z <span class="nv">$selected</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">exit</span> <span class="m">0</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">selected_name</span><span class="o">=</span><span class="k">$(</span>basename <span class="s2">&#34;</span><span class="nv">$selected</span><span class="s2">&#34;</span> <span class="p">|</span> tr . _<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">tmux_config_file</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$selected</span><span class="s2">/.tmuxinator.yml&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> -f <span class="s2">&#34;</span><span class="nv">$tmux_config_file</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">cd</span> <span class="nv">$selected</span>
</span></span><span class="line"><span class="cl">  tmuxinator <span class="nb">local</span>
</span></span><span class="line"><span class="cl">  <span class="nb">cd</span> -
</span></span><span class="line"><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="cl">  tmuxinator start project -n <span class="nv">$selected_name</span> <span class="nv">workspace</span><span class="o">=</span><span class="nv">$selected</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span></code></pre></div><p>This is flexible enough to have custom configurations when needed or just use the default one!</p>
<p>I also added <code>.tmuxinator.yml</code> to my <code>~/.gitignore</code>, so it doesn&rsquo;t pollute my git projects with tmux configuration files.</p>
<pre tabindex="0"><code># ~/.gitignore
...
.tmuxinator.yml
</code></pre><p>Finally, I added a keybinding to start the <code>tmux-sessionizer</code> on my tmux config:</p>
<pre tabindex="0"><code># .config/tmux/tmux.conf
...
# Start tmux session on project
bind-key -r f run-shell &#34;tmux neww ~/.local/bin/tmux-sessionizer&#34;
</code></pre><p>Result:</p>
<p><img loading="lazy" src="/posts/tmux-tmuxinator-fzf-workflow/output.webp" type="" alt="A video showing  the command being executed, creating a new tmux session"  /></p>
]]></content:encoded></item></channel></rss>