<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem</title>
    <description>The most recent home feed on Forem.</description>
    <link>https://forem.com</link>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed"/>
    <language>en</language>
    <item>
      <title>Making illegal state unrepresentable</title>
      <dc:creator>Nicolas Fränkel</dc:creator>
      <pubDate>Thu, 23 Apr 2026 09:02:00 +0000</pubDate>
      <link>https://forem.com/nfrankel/making-illegal-state-unrepresentable-3bid</link>
      <guid>https://forem.com/nfrankel/making-illegal-state-unrepresentable-3bid</guid>
      <description>&lt;p&gt;A couple of years ago, I wrote that &lt;a href="https://blog.frankel.ch/builder-pattern-finite-state-machine/" rel="noopener noreferrer"&gt;The Builder pattern is a finite state machine!&lt;/a&gt;. A state machine consists of states and transitions between them. As a developer, I want to make illegal states unrepresentable, &lt;em&gt;i.e.&lt;/em&gt;, users of my API can't create non-existent transitions. My hypothesis is that only a static typing system allows this at compile-time. Dynamic typing systems rely on runtime validation.&lt;/p&gt;

&lt;p&gt;In this blog post, I will show that it holds true, with a caveat. If your model has many combinations, you also need generics and other niceties to avoid too much boilerplate code. My exploration will use Python, Java, Kotlin, Rust, and Gleam. With that background in mind, let's move on to the model itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The model
&lt;/h2&gt;

&lt;p&gt;I will keep the theme of the aforementioned post: a pizza. Here's the full model:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fazler9i8x32zh734otki.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fazler9i8x32zh734otki.png" alt="States-transitions diagram for a simple pizza builder"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every pizza starts with dough. Then, you need to choose your base, either regular tomato or cream. At this point, you can't add any ingredients to it. While you can add ham to both, pineapple is only acceptable on the tomato base, while potatoes are only compatible with the cream base.&lt;/p&gt;

&lt;p&gt;The model is purposely simplistic, intentionally leaving out some options and complexities to maintain clarity. It primarily showcases how illegal transitions are represented and avoided. For example, you can't transition from &lt;code&gt;CreamBase&lt;/code&gt; to &lt;code&gt;CreamBase&lt;/code&gt; with &lt;code&gt;withPineapple&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modeling with Python
&lt;/h2&gt;

&lt;p&gt;I chose to model with Python first to demonstrate how dynamically-typed languages manage illegal transitions.&lt;/p&gt;

&lt;p&gt;Here are some types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Crust&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ThinCrust&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Crust&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ThickCrust&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Crust&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreamBase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TomatoBase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Topping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Potatoes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Topping&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Pineapple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Topping&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The builders look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DoughBuilder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Crust&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_crust&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;crust&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;with_cream_base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CreamBuilder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;CreamBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;CreamBase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;frozenset&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;with_tomato_base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TomatoBuilder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;TomatoBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TomatoBase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;frozenset&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreamBuilder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CreamBase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toppings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;frozenset&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Topping&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_crust&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;crust&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_toppings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;toppings&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;with_potatoes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CreamBuilder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;CreamBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_toppings&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nc"&gt;Potatoes&lt;/span&gt;&lt;span class="p"&gt;()})&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;with_ham&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CreamBuilder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;CreamBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_toppings&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nc"&gt;Ham&lt;/span&gt;&lt;span class="p"&gt;()})&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;with_olives&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CreamBuilder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;CreamBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_toppings&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nc"&gt;Olives&lt;/span&gt;&lt;span class="p"&gt;()})&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Pizza&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Pizza&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_toppings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TomatoBuilder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TomatoBase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toppings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;frozenset&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Topping&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_crust&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;crust&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_toppings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;toppings&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;with_pineapple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TomatoBuilder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;TomatoBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_toppings&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nc"&gt;Pineapple&lt;/span&gt;&lt;span class="p"&gt;()})&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;with_ham&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TomatoBuilder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;TomatoBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_toppings&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nc"&gt;Ham&lt;/span&gt;&lt;span class="p"&gt;()})&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;with_olives&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TomatoBuilder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;TomatoBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_toppings&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nc"&gt;Olives&lt;/span&gt;&lt;span class="p"&gt;()})&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Pizza&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Pizza&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_toppings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can happily write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pizza&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dough&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ThinCrust&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;with_cream_base&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;with_potatoes&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;with_ham&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;pizza&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dough&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ThinCrust&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;with_cream_base&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;with_pineapple&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;with_ham&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# 1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Pineapple on a creamy base? Yuck!&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Astute readers may have noticed that I used types in the Python code. Seasoned Python developers may use a type checker, &lt;em&gt;e.g.&lt;/em&gt;, &lt;code&gt;mypy&lt;/code&gt;, to leverage them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run mypy &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"CreamBuilder" has no attribute "with_pineapple"  [attr-defined]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;mypy&lt;/code&gt;, the call fails at runtime with an &lt;code&gt;AttributeError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Python's gradual typing proves that static typing helps avoid calling non-existent transitions. However, because of the way Python works, one needs to invoke a dedicated type checker. In statically-typed languages, it's unnecessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modeling with Java
&lt;/h2&gt;

&lt;p&gt;Let's do the same exercise with Java.&lt;/p&gt;

&lt;p&gt;Here's the model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;Crust&lt;/span&gt; &lt;span class="n"&gt;permits&lt;/span&gt; &lt;span class="nc"&gt;ThinCrust&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ThickCrust&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;ThinCrust&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Crust&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;ThickCrust&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Crust&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;

&lt;span class="n"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;BaseState&lt;/span&gt; &lt;span class="n"&gt;permits&lt;/span&gt; &lt;span class="nc"&gt;CreamBase&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TomatoBase&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;CreamBase&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;BaseState&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;TomatoBase&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;BaseState&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here are the builders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DoughBuilder&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Crust&lt;/span&gt; &lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;DoughBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Crust&lt;/span&gt; &lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;crust&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;DoughBuilder&lt;/span&gt; &lt;span class="nf"&gt;dough&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Crust&lt;/span&gt; &lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DoughBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;CreamBaseBuilder&lt;/span&gt; &lt;span class="nf"&gt;withCreamBase&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreamBaseBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CreamBase&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;TomatoBaseBuilder&lt;/span&gt; &lt;span class="nf"&gt;withTomatoBase&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TomatoBaseBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TomatoBase&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreamBaseBuilder&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Crust&lt;/span&gt; &lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;CreamBase&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Topping&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;toppings&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nc"&gt;CreamBaseBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Crust&lt;/span&gt; &lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;CreamBase&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;CreamBaseBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Crust&lt;/span&gt; &lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;CreamBase&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Topping&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;toppings&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;crust&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toppings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;toppings&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;CreamBaseBuilder&lt;/span&gt; &lt;span class="nf"&gt;withPotatoes&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreamBaseBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Potatoes&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;CreamBaseBuilder&lt;/span&gt; &lt;span class="nf"&gt;withHam&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreamBaseBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Ham&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;CreamBaseBuilder&lt;/span&gt; &lt;span class="nf"&gt;withOlives&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreamBaseBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Olives&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;Pizza&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Pizza&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyOf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toppings&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Topping&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Topping&lt;/span&gt; &lt;span class="n"&gt;topping&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;concat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toppings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;Stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topping&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TomatoBaseBuilder&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Crust&lt;/span&gt; &lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;TomatoBase&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Topping&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;toppings&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nc"&gt;TomatoBaseBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Crust&lt;/span&gt; &lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TomatoBase&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;TomatoBaseBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Crust&lt;/span&gt; &lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TomatoBase&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Topping&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;toppings&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;crust&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toppings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;toppings&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;TomatoBaseBuilder&lt;/span&gt; &lt;span class="nf"&gt;withPineapple&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TomatoBaseBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Pineapple&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;TomatoBaseBuilder&lt;/span&gt; &lt;span class="nf"&gt;withHam&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TomatoBaseBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Ham&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;TomatoBaseBuilder&lt;/span&gt; &lt;span class="nf"&gt;withOlives&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TomatoBaseBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Olives&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;Pizza&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Pizza&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyOf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toppings&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Topping&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Topping&lt;/span&gt; &lt;span class="n"&gt;topping&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;concat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toppings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;Stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topping&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no way we can call &lt;code&gt;withPineapple()&lt;/code&gt; on a &lt;code&gt;CreamBase&lt;/code&gt;: we didn't declare the method, and compilation fails. However, imagine more combinations: an additional sweet base, or ingredients incompatible with others, &lt;em&gt;e.g.&lt;/em&gt;, no anchovies with pineapple. This would create additional states, represented as classes, and transitions, as methods. Soon, you'll face an explosion of combinations, which would require lots of boilerplate code. For example, with the anchovies/pineapple exclusion, you'd need two more base classes: &lt;code&gt;TomatoBaseWithPineappleBuilder&lt;/code&gt; and &lt;code&gt;TomatoBaseWithAnchoviesBuilder&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Java is not a good fit for creating smart builders. Let's see if other languages may improve the situation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modeling with Kotlin
&lt;/h2&gt;

&lt;p&gt;The Kotlin model is pretty similar to the Java one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;Crust&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;ThinCrust&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Crust&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;ThickCrust&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Crust&lt;/span&gt;

&lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;BaseState&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;CreamBase&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BaseState&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;TomatoBase&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BaseState&lt;/span&gt;

&lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;Topping&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;Ham&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Topping&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;Olives&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Topping&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;Potatoes&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Topping&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;Pineapple&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Topping&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The builder approach is pretty different, though:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DoughBuilder&lt;/span&gt; &lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;crust&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Crust&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PizzaBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="nc"&gt;S&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BaseState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="c1"&gt;// 1 // 2&lt;/span&gt;
    &lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;crust&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BaseState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;toppings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Topping&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;dough&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Crust&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;DoughBuilder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DoughBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nc"&gt;DoughBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withCreamBase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="nc"&gt;PizzaBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CreamBase&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;CreamBase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;emptyList&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;                         &lt;span class="c1"&gt;// 3&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nc"&gt;DoughBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withTomatoBase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="nc"&gt;PizzaBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TomatoBase&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TomatoBase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;emptyList&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;                       &lt;span class="c1"&gt;// 3&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;PizzaBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CreamBase&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;withPotatoes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="nc"&gt;PizzaBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CreamBase&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toppings&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="nc"&gt;Potatoes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                      &lt;span class="c1"&gt;// 3&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;PizzaBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TomatoBase&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;withPineapple&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="nc"&gt;PizzaBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TomatoBase&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toppings&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="nc"&gt;Pineapple&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                    &lt;span class="c1"&gt;// 3&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;S&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BaseState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;PizzaBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;S&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;withHam&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="nc"&gt;PizzaBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;S&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toppings&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="nc"&gt;Ham&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                   &lt;span class="c1"&gt;// 3 // 4&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;S&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BaseState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;PizzaBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;S&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;withOlives&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="nc"&gt;PizzaBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;S&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toppings&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="nc"&gt;Olives&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                &lt;span class="c1"&gt;// 3 // 4&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;S&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BaseState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;PizzaBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;S&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Pizza&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toppings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toSet&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Generics on the parent class are possible in Java, but it lacks what comes next.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;out&lt;/code&gt; is Kotlin's syntax for &lt;em&gt;covariance&lt;/em&gt;: a &lt;code&gt;PizzaBuilder&amp;lt;CreamBase&amp;gt;&lt;/code&gt; can be used where a &lt;code&gt;PizzaBuilder&amp;lt;BaseState&amp;gt;&lt;/code&gt; is expected. The upper bound &lt;code&gt;: BaseState&lt;/code&gt; separately constrains &lt;code&gt;S&lt;/code&gt; to &lt;code&gt;BaseState&lt;/code&gt; and its subclasses.&lt;/li&gt;
&lt;li&gt;Function extensions are Kotlin-specific. Instead of creating different subclasses as in Java, we keep a single one, but extend each &lt;strong&gt;generic type&lt;/strong&gt; with the relevant function.&lt;/li&gt;
&lt;li&gt;For transitions that are common to both bases, generics help us return the same type. This way, we can define the function once on the generic type.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If we were to add pineapple/anchovies exclusion as in Java, we would need a slight change in the existing hierarchy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;TomatoBase&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BaseState&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;Tomato&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TomatoBase&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then, add the states and the transitions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;TomatoWithPineapple&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TomatoBase&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;TomatoWithAnchovies&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TomatoBase&lt;/span&gt;

&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;PizzaBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TomatoBase&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;withPineapple&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;PizzaBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TomatoWithPineapple&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TODO&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;PizzaBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TomatoBase&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;withAnchovies&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;PizzaBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TomatoWithAnchovies&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TODO&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, you'd define the exclusive transitions on the new types with extension functions.&lt;/p&gt;

&lt;p&gt;In Kotlin, unlike Java, adding a new constraint requires only new type markers and extension functions — the growth is linear, not combinatorial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modeling with Rust
&lt;/h2&gt;

&lt;p&gt;Rust isn't an Object-Oriented programming language, contrary to Kotlin. Let's examine what we can learn from it.&lt;/p&gt;

&lt;p&gt;The model part is quite straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[derive(Debug,&lt;/span&gt; &lt;span class="nd"&gt;Clone,&lt;/span&gt; &lt;span class="nd"&gt;Copy,&lt;/span&gt; &lt;span class="nd"&gt;PartialEq,&lt;/span&gt; &lt;span class="nd"&gt;Eq)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;Crust&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Thin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Thick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[derive(Debug,&lt;/span&gt; &lt;span class="nd"&gt;Clone,&lt;/span&gt; &lt;span class="nd"&gt;Copy,&lt;/span&gt; &lt;span class="nd"&gt;PartialEq,&lt;/span&gt; &lt;span class="nd"&gt;Eq)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;Base&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Cream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Tomato&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;HasDough&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;CreamBase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;TomatoBase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Pizza&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ham&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;olives&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;potatoes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pineapple&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Interestingly enough, the design is pretty similar to Java's:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[derive(Default)]&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;ToppingState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ham&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;olives&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;potatoes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pineapple&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;PizzaBuilder&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;toppings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ToppingState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_marker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;PhantomData&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                          &lt;span class="c1"&gt;// 1&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;PizzaBuilder&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HasDough&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;dough&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Crust&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PizzaBuilder&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HasDough&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;PizzaBuilder&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;toppings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;ToppingState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;_marker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;PhantomData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                     &lt;span class="c1"&gt;// 1&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;with_cream_base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PizzaBuilder&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreamBase&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;PizzaBuilder&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;toppings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.toppings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;_marker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;PhantomData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                     &lt;span class="c1"&gt;// 1&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;with_tomato_base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PizzaBuilder&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TomatoBase&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;PizzaBuilder&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;crust&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;toppings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.toppings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;_marker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;PhantomData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                     &lt;span class="c1"&gt;// 1&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;PizzaBuilder&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreamBase&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;with_potatoes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.toppings.potatoes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;with_ham&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.toppings.ham&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;with_olives&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.toppings.olives&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Pizza&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;Pizza&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nn"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Cream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.toppings.ham&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.toppings.olives&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.toppings.potatoes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.toppings.pineapple&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;PizzaBuilder&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TomatoBase&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;with_pineapple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.toppings.pineapple&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;with_ham&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.toppings.ham&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;with_olives&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.toppings.olives&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Pizza&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;Pizza&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.crust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nn"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Tomato&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.toppings.ham&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.toppings.olives&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.toppings.potatoes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.toppings.pineapple&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Phantom type. See section Phantom types below.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Java and Rust have fundamental differences in approaches: the former uses a runtime, and the latter compiles to native. Yet, from a pure language syntax point of view, Rust &lt;code&gt;impl&lt;/code&gt; maps more or less to a Java subclass in this context.&lt;/p&gt;

&lt;p&gt;We would get the same combinatory explosion for anchovies and pineapple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modeling with Gleam
&lt;/h2&gt;

&lt;p&gt;Gleam is a recent programming language that runs on the &lt;a href="https://en.wikipedia.org/wiki/BEAM_(Erlang_virtual_machine)" rel="noopener noreferrer"&gt;BEAM&lt;/a&gt;. As of now, I'd say I'm proficient in Java and Kotlin, thus covering the JVM. Kotlin also allows JavaScript transpilation for the browser. I'm able to write quite a bit in Python, and consider myself a Rust newbie. The BEAM is outside my reach, and I wanted to widen my options. Erlang is dynamically-typed, and Elixir is a bit too esoteric. &lt;a href="https://tour.gleam.run/" rel="noopener noreferrer"&gt;Gleam's syntax&lt;/a&gt; is a bit similar to Rust's.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Given my limited knowledge of Gleam, I used Claude Code &lt;strong&gt;a lot&lt;/strong&gt; in this section. I'll be happy to receive feedback from Gleam programmers as I'm still learning.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Types are declared as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub type Crust {
  Thin
  Thick
}

pub type Topping {
  Potatoes
  Pineapple
  Ham
  Olives
}

pub type Base {
  CreamBase
  TomatoBase
}

pub type Cream {
  Cream
}

pub type Tomato {
  Tomato
}

pub opaque type Pizza {                                                            // 1
  Pizza(crust: Crust, base: Base, toppings: List(Topping))
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;opaque&lt;/code&gt; is Gleam's way to hide the constructor from outside the module&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then the builder looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub opaque type DoughBuilder {                                                     // 1
  DoughBuilder(crust: Crust)
}

pub opaque type PizzaBuilder(base) {                                               // 1
  PizzaBuilder(crust: Crust, base: Base, toppings: List(Topping))
}

pub fn dough(crust: Crust) -&amp;gt; DoughBuilder {
  DoughBuilder(crust: crust)
}

pub fn with_cream_base(builder: DoughBuilder) -&amp;gt; PizzaBuilder(Cream) {
  let DoughBuilder(crust: crust) = builder
  PizzaBuilder(crust: crust, base: CreamBase, toppings: [])
}

pub fn with_tomato_base(builder: DoughBuilder) -&amp;gt; PizzaBuilder(Tomato) {
  let DoughBuilder(crust: crust) = builder
  PizzaBuilder(crust: crust, base: TomatoBase, toppings: [])
}

pub fn with_potatoes(builder: PizzaBuilder(Cream)) -&amp;gt; PizzaBuilder(Cream) {        // 2
  let PizzaBuilder(crust: crust, base: base, toppings: toppings) = builder
  PizzaBuilder(crust: crust, base: base, toppings: [Potatoes, ..toppings])
}

pub fn with_pineapple(builder: PizzaBuilder(Tomato)) -&amp;gt; PizzaBuilder(Tomato) {     // 2
  let PizzaBuilder(crust: crust, base: base, toppings: toppings) = builder
  PizzaBuilder(crust: crust, base: base, toppings: [Pineapple, ..toppings])
}

pub fn with_ham(builder: PizzaBuilder(a)) -&amp;gt; PizzaBuilder(a) {                     // 3
  let PizzaBuilder(crust: crust, base: base, toppings: toppings) = builder
  PizzaBuilder(crust: crust, base: base, toppings: [Ham, ..toppings])
}

pub fn with_olives(builder: PizzaBuilder(a)) -&amp;gt; PizzaBuilder(a) {                  // 3
  let PizzaBuilder(crust: crust, base: base, toppings: toppings) = builder
  PizzaBuilder(crust: crust, base: base, toppings: [Olives, ..toppings])
}

pub fn build(builder: PizzaBuilder(a)) -&amp;gt; Pizza {
  let PizzaBuilder(crust: crust, base: base, toppings: toppings) = builder
  Pizza(crust: crust, base: base, toppings: list.reverse(toppings))
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Hide the constructor from outside the module&lt;/li&gt;
&lt;li&gt;Specific transitions&lt;/li&gt;
&lt;li&gt;Common transitions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Adding anchovies topping exclusive to pineapple would require: adding the new states as types and the transitions as functions. As in Kotlin, it would be a linear code expansion, thanks to the generic &lt;code&gt;PizzaBuilder(a)&lt;/code&gt; parameter and return type for common transitions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phantom types
&lt;/h2&gt;

&lt;p&gt;Kotlin, Rust, and Gleam all allow a generic type on &lt;code&gt;PizzaBuilder&lt;/code&gt;. While the syntax is different and the naming differs, phantom data in Rust, phantom type in Gleam, the concept stays the same: use generic types to enforce typing &lt;strong&gt;at compilation time&lt;/strong&gt;, while letting go of them at runtime.&lt;/p&gt;

&lt;p&gt;Here's the Kotlin code above, simplified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PizzaBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="nc"&gt;S&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BaseState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;PizzaBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CreamBase&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;withPotatoes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TODO&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;PizzaBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TomatoBase&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;withPineapple&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TODO&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The magic is the &lt;code&gt;S&lt;/code&gt; generic in the &lt;code&gt;PizzaBuilder&lt;/code&gt;. It allows grafting extension functions based on this type. &lt;code&gt;withPotatoes()&lt;/code&gt; only applies when &lt;code&gt;S&lt;/code&gt; is &lt;code&gt;CreamBase&lt;/code&gt;, &lt;code&gt;withPineapple()&lt;/code&gt; when it's &lt;code&gt;TomatoBase&lt;/code&gt;. Without a generic type, you can't constrain extension functions. However, &lt;code&gt;S&lt;/code&gt; isn't used in any other place: it's effectively a phantom type, hence the name.&lt;/p&gt;

&lt;p&gt;Gleam splits between types and functions. While this design is quite different from Kotlin's, it also enables phantom types to constrain functions depending on the phantom type.&lt;/p&gt;

&lt;p&gt;The Rust compiler effectively disallows unused types, so you need to reference it somehow. Hence, you need a &lt;code&gt;_marker: std::marker::PhantomData&lt;/code&gt; attribute: it's a zero-cost abstraction, discarded in the compiled code and useful only for the compiler. The leading underscore is a Rust convention that suppresses Rust's dead-code warning.&lt;/p&gt;

&lt;p&gt;Note that despite Java having generics, its lack of extension functions or related mechanisms prevents attaching methods to a specific generic type.&lt;/p&gt;

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

&lt;p&gt;Static typing is the solid foundation that prevents illegal state transitions. Yet, you need more to design a model that doesn't degrade quickly. Without phantom types and a way to leverage them to bind specific functions to them, each new state adds combinatorial complexity. The exact mechanism depends on the language: extension functions in Kotlin, impl blocks in Rust, and constrained parameter types in Gleam.&lt;/p&gt;

&lt;p&gt;The complete source code for this post can be found on &lt;a href="https://codeberg.org/ajavageek/kotlin-rust-gleam" rel="noopener noreferrer"&gt;Codeberg&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To go further:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.frankel.ch/builder-pattern-finite-state-machine/" rel="noopener noreferrer"&gt;The Builder pattern is a finite state machine!&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://inside.java/2024/06/03/dop-v1-1-illegal-states/" rel="noopener noreferrer"&gt;Make Illegal States Unrepresentable - Data-Oriented Programming v1.1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://blog.frankel.ch/illegal-state-unrepresentable/" rel="noopener noreferrer"&gt;A Java Geek&lt;/a&gt; on April 19&lt;sup&gt;th&lt;/sup&gt;, 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>coding</category>
      <category>java</category>
      <category>kotlin</category>
    </item>
    <item>
      <title>Stop memorizing JS — think in execution context</title>
      <dc:creator>Samaresh Das</dc:creator>
      <pubDate>Thu, 23 Apr 2026 09:00:55 +0000</pubDate>
      <link>https://forem.com/samareshdas/stop-memorizing-js-think-in-execution-context-2ne4</link>
      <guid>https://forem.com/samareshdas/stop-memorizing-js-think-in-execution-context-2ne4</guid>
      <description>&lt;p&gt;Forget remembering JavaScript syntax; it's a crutch.&lt;/p&gt;

&lt;p&gt;What if I told you that the key to truly &lt;em&gt;understanding&lt;/em&gt; JavaScript isn't memorizing endless functions and methods, but grasping how the code actually runs? This article is about shifting your focus from rote learning to thinking about JavaScript's execution context.&lt;/p&gt;

&lt;p&gt;Every time your JavaScript code runs, the engine creates an "execution context." Think of it as a mini-environment where your code gets evaluated. There are two main types: the Global Execution Context (the default one for your entire script) and the Function Execution Context (created each time a function is called).&lt;/p&gt;

&lt;p&gt;Within each context, two crucial things happen: the creation phase and the execution phase. During creation, the engine sets up the &lt;code&gt;Variable Environment&lt;/code&gt; (where &lt;code&gt;var&lt;/code&gt;, &lt;code&gt;let&lt;/code&gt;, &lt;code&gt;const&lt;/code&gt;, and function declarations are stored) and the &lt;code&gt;this&lt;/code&gt; binding. It also creates the &lt;code&gt;scope chain&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then comes the execution phase, where the actual JavaScript code is run line by line. This is where &lt;code&gt;let&lt;/code&gt; and &lt;code&gt;const&lt;/code&gt; declarations are put into a "temporal dead zone" until their declaration is reached, while &lt;code&gt;var&lt;/code&gt; variables are initialized with &lt;code&gt;undefined&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Let's look at a simple example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// undefined&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 10&lt;/span&gt;

&lt;span class="c1"&gt;// sayHello(); // This would throw an error&lt;/span&gt;
&lt;span class="c1"&gt;// let sayHello = () =&amp;gt; {&lt;/span&gt;
&lt;span class="c1"&gt;//   console.log("Hello!");&lt;/span&gt;
&lt;span class="c1"&gt;// };&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first &lt;code&gt;console.log(a)&lt;/code&gt; outputs &lt;code&gt;undefined&lt;/code&gt; because &lt;code&gt;var a&lt;/code&gt; is hoisted to the top of its scope and initialized with &lt;code&gt;undefined&lt;/code&gt; during the creation phase, &lt;em&gt;before&lt;/em&gt; the execution phase reaches &lt;code&gt;var a = 10;&lt;/code&gt;. If &lt;code&gt;a&lt;/code&gt; were declared with &lt;code&gt;let&lt;/code&gt; or &lt;code&gt;const&lt;/code&gt;, the first &lt;code&gt;console.log&lt;/code&gt; would actually throw a &lt;code&gt;ReferenceError&lt;/code&gt; because of the temporal dead zone.&lt;/p&gt;

&lt;p&gt;Understanding execution context helps untangle confusing behavior, especially with asynchronous operations and closures. It's the "why" behind what your code is doing.&lt;/p&gt;

&lt;p&gt;For me, this mindset shift was a game-changer. It's what allows me to build complex websites and apps as a freelancer. If you're ever looking for someone to build your next web project, you can check out my portfolio here: &lt;a href="https://hire-sam.vercel.app/" rel="noopener noreferrer"&gt;https://hire-sam.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The real superpower in JavaScript isn't memorization, it's understanding the runtime.&lt;/p&gt;

&lt;p&gt;Follow for more dev content&lt;/p&gt;

</description>
      <category>javascript</category>
    </item>
    <item>
      <title>Building a Privacy-First URL Shortener on Blockchain</title>
      <dc:creator>Wes Parsons</dc:creator>
      <pubDate>Thu, 23 Apr 2026 09:00:02 +0000</pubDate>
      <link>https://forem.com/wes_parsons_57932a30dc1f3/building-a-privacy-first-url-shortener-on-blockchain-323i</link>
      <guid>https://forem.com/wes_parsons_57932a30dc1f3/building-a-privacy-first-url-shortener-on-blockchain-323i</guid>
      <description>&lt;h1&gt;
  
  
  Why Traditional URL Shorteners Are a Privacy Nightmare
&lt;/h1&gt;

&lt;p&gt;When you click a bit.ly link, here's what happens:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Bit.ly logs your IP, timestamp, user agent&lt;/li&gt;
&lt;li&gt;They see the destination URL&lt;/li&gt;
&lt;li&gt;They track your browsing patterns&lt;/li&gt;
&lt;li&gt;They sell this data to advertisers&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Even if you trust the shortener, their database can be hacked.&lt;/strong&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Introducing Cryptly
&lt;/h1&gt;

&lt;p&gt;I built cryptly to solve this problem using blockchain and encryption.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Encryption (Client-Side)&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
     &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AES-GCM&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
     &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="nx"&gt;urlBuffer&lt;/span&gt;
   &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Blockchain Storage&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Encrypted URL stored on Cronos blockchain&lt;/li&gt;
&lt;li&gt;Immutable, decentralized&lt;/li&gt;
&lt;li&gt;No centralized database to hack&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Decryption (Browser)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Browser fetches from blockchain&lt;/li&gt;
&lt;li&gt;Decrypts locally using Web Crypto API&lt;/li&gt;
&lt;li&gt;Server never sees destination&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tech Stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cloudflare Workers (serverless, edge deployment)&lt;/li&gt;
&lt;li&gt;Web Crypto API (native browser encryption)&lt;/li&gt;
&lt;li&gt;Cronos blockchain (decentralized storage)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Privacy Benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Server never sees destination URLs&lt;/li&gt;
&lt;li&gt;✅ No tracking, no analytics&lt;/li&gt;
&lt;li&gt;✅ No database to leak&lt;/li&gt;
&lt;li&gt;✅ Censorship-resistant (blockchain)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;Live demo: &lt;a href="https://cryptly.workers.dev" rel="noopener noreferrer"&gt;cryptly.workers.dev&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/your-username/cryptly" rel="noopener noreferrer"&gt;github.com/your-username/cryptly&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Still in early stages but feedback welcome!&lt;/p&gt;

&lt;h1&gt;
  
  
  privacy #blockchain #webdev #opensource
&lt;/h1&gt;

</description>
      <category>privacy</category>
      <category>blockchain</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>CQRS in Go — Part 4: PostgreSQL as an event store</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Thu, 23 Apr 2026 09:00:02 +0000</pubDate>
      <link>https://forem.com/ohugonnot/cqrs-in-go-part-4-postgresql-as-an-event-store-be4</link>
      <guid>https://forem.com/ohugonnot/cqrs-in-go-part-4-postgresql-as-an-event-store-be4</guid>
      <description>&lt;p&gt;CQRS in Go series:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://www.web-developpeur.com/en/blog/cqrs-go-aggregate-transition-clone" rel="noopener noreferrer"&gt;&lt;strong&gt;Part 1&lt;/strong&gt;: the aggregate, Transition() and Clone()&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://www.web-developpeur.com/en/blog/cqrs-go-command-handlers-testabilite" rel="noopener noreferrer"&gt;&lt;strong&gt;Part 2&lt;/strong&gt;: command handlers without side effects&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://www.web-developpeur.com/en/blog/cqrs-go-sagas-choreographie-events" rel="noopener noreferrer"&gt;&lt;strong&gt;Part 3&lt;/strong&gt;: sagas and event choreography&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Part 4&lt;/strong&gt;: PostgreSQL as an event store&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first three parts laid the groundwork: immutable aggregates, pure command handlers, sagas through choreography. One central question remains — where do we persist the events? This part answers that question using PostgreSQL as an append-only event store, with no additional dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why PostgreSQL and not EventStoreDB or Kafka
&lt;/h2&gt;

&lt;p&gt;EventStoreDB is an excellent tool, designed specifically for event sourcing. But it's an additional component to deploy, monitor, back up, and maintain. For a team already running PostgreSQL in production, adding EventStoreDB means doubling the operational footprint for a database.&lt;/p&gt;

&lt;p&gt;Kafka is often mentioned in this context. It's a message bus, not an event store. Reading by aggregate ID is not its strong suit — Kafka is optimized for sequential partition consumption, not for "give me all events for order CMD-4521 in order". Default retention is time-limited. Reconstituting an aggregate from Kafka requires non-trivial work.&lt;/p&gt;

&lt;p&gt;PostgreSQL, you already have it. Native ACID, JSONB with indexing, backups integrated into your existing infrastructure, monitoring your team knows, pg_dump, replication, point-in-time recovery. For 90% of projects, an append-only table in PostgreSQL is more than enough. Moving to a dedicated tool makes sense when you're processing millions of events per second — a threshold most projects never reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The es_events table — the core
&lt;/h2&gt;

&lt;p&gt;Everything rests on a single table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;es_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;            &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;aggregate_id&lt;/span&gt;  &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;aggregate_type&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt;    &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_data&lt;/span&gt;    &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;metadata&lt;/span&gt;      &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;version&lt;/span&gt;       &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;    &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;

    &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aggregate_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_es_events_aggregate&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;es_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aggregate_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each column has a specific role:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;id (BIGSERIAL)&lt;/strong&gt;: global position in the stream. This is the cursor used by projectors to track where they are. Monotonically increasing, never reused.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;aggregate_id&lt;/strong&gt;: the identifier of the entity involved — the UUID of the order, payment, or shipment. This is the primary read key for reconstituting an aggregate.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;aggregate_type&lt;/strong&gt;: the type of the aggregate — "order", "payment", "shipping". Allows filtering events by domain and avoids UUID collisions between different types.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;event_type&lt;/strong&gt;: the name of the event — "OrderPlaced", "PaymentConfirmed", "ShipmentDispatched". Used to deserialize &lt;code&gt;event_data&lt;/code&gt; into the correct Go type.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;event_data (JSONB)&lt;/strong&gt;: the serialized event payload. JSONB enables indexing and field-level queries when needed, without sacrificing flexibility.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;metadata (JSONB)&lt;/strong&gt;: traceability information — who triggered the action (user ID), correlation ID, causation ID, client timestamp. Not domain data, but indispensable in production.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;version&lt;/strong&gt;: sequence number per aggregate. The first event of an aggregate is at version 1, the second at version 2, and so on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;UNIQUE (aggregate_id, version)&lt;/code&gt; constraint is the centerpiece of concurrent safety. It makes it impossible to insert two events with the same version on the same aggregate. This is the optimistic locking mechanism — no read lock, a constraint violation on write in case of conflict.&lt;/p&gt;

&lt;h2&gt;
  
  
  Append — writing an event
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;EventStore&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sqlx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;EventStore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aggregateID&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aggregateType&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expectedVersion&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;StorableEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BeginTxx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"begin tx: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rollback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;expectedVersion&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;

        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"marshal event: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;`
            INSERT INTO es_events (aggregate_id, aggregate_type, event_type, event_data, metadata, version)
            VALUES ($1, $2, $3, $4, $5, $6)
        `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aggregateID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aggregateType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c"&gt;// UNIQUE violation = version conflict = optimistic locking&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;isUniqueViolation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ErrConcurrencyConflict&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"insert event: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;expectedVersion&lt;/code&gt; parameter is the number of the last known event at the time the aggregate was loaded. If the handler produces two events, they will be inserted at versions &lt;code&gt;expectedVersion + 1&lt;/code&gt; and &lt;code&gt;expectedVersion + 2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If another goroutine wrote an event between loading and writing, the UNIQUE constraint kicks in — PostgreSQL rejects the insert with a constraint violation, translated into &lt;code&gt;ErrConcurrencyConflict&lt;/code&gt;. The client can retry: it will reload the aggregate with the new event, recalculate the state, and re-execute the handler. This is optimistic locking — no read lock, just a check at write time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Load — reloading an aggregate
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;EventStore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aggregateID&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="n"&gt;StoredEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;StoredEvent&lt;/span&gt;

    &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SelectContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;`
        SELECT id, aggregate_id, aggregate_type, event_type, event_data, metadata, version, created_at
        FROM es_events
        WHERE aggregate_id = $1
        ORDER BY version ASC
    `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aggregateID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"loading events: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The aggregate's state is never stored directly. It's always recalculated by replaying events in order, calling &lt;code&gt;Transition()&lt;/code&gt; on each one (see Part 1). This is the replay. The result is deterministic — the same events always produce the same state. This is what makes the system testable and auditable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Subscriptions — feeding projections
&lt;/h2&gt;

&lt;p&gt;Projectors — the components that maintain read views — must be notified of new events. The simplest solution: a positions table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;es_subscriptions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;subscriber_id&lt;/span&gt;  &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_event_id&lt;/span&gt;  &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;updated_at&lt;/span&gt;     &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each projector records the identifier of the last event it processed. On startup or after a crash, it resumes from this position. The polling pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Subscriber&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;           &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sqlx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;
    &lt;span class="n"&gt;subscriberID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;handler&lt;/span&gt;      &lt;span class="n"&gt;EventHandler&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Subscriber&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Poll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// 1. Read the last processed position&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;lastID&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lastID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"SELECT last_event_id FROM es_subscriptions WHERE subscriber_id = $1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subscriberID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// 2. Load new events&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;StoredEvent&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SelectContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"SELECT * FROM es_events WHERE id &amp;gt; $1 ORDER BY id ASC LIMIT 100"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;lastID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// 3. Process within a transaction&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BeginTxx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rollback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"handling event %d: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c"&gt;// 4. Update the position in the same transaction&lt;/span&gt;
        &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"UPDATE es_subscriptions SET last_event_id = $1, updated_at = NOW() WHERE subscriber_id = $2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subscriberID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The critical detail is in point 4: the position update and event processing are in the same transaction. If the process crashes between processing and updating the position, the event will be reprocessed on the next poll. This is at-least-once delivery — handlers must be idempotent to absorb duplicates without side effects.&lt;/p&gt;

&lt;p&gt;Idempotency in practice: a handler that inserts a row into a read view can use &lt;code&gt;INSERT ... ON CONFLICT DO NOTHING&lt;/code&gt;, or check whether the row already exists. The deduplication key is generally the &lt;code&gt;event_id&lt;/code&gt; or a combination of unique business fields.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-time notifications with LISTEN/NOTIFY
&lt;/h2&gt;

&lt;p&gt;Polling introduces latency — at best a few hundred milliseconds if the poll runs frequently, at worst several seconds. For read views that need to be reactive, PostgreSQL offers a native mechanism: LISTEN/NOTIFY.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Event store side: notify after each INSERT&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;EventStore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aggregateID&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aggregateType&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expectedVersion&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;StorableEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BeginTxx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"begin tx: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rollback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c"&gt;// ... insert events ...&lt;/span&gt;

    &lt;span class="c"&gt;// Notify subscribers&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"NOTIFY es_events_channel"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"notify: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Subscriber side: listen instead of polling&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Subscriber&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Conn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"acquire conn: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"LISTEN es_events_channel"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"listen: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c"&gt;// Wait for notification (blocking with timeout)&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitForNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"wait notification: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c"&gt;// Poll immediately after the notification&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Poll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The notification is sent inside the Append transaction — it's only delivered to listeners once the transaction commits. No false alarms on rollback.&lt;/p&gt;

&lt;p&gt;In production, combine both approaches: LISTEN for real-time reactivity under normal conditions, periodic polling (every 5 to 30 seconds) as a safety net in case of reconnection or missed notification. The dedicated LISTEN connection must not be taken from the standard connection pool — it occupies a connection permanently.&lt;/p&gt;

&lt;h2&gt;
  
  
  The command gateway — putting it all together
&lt;/h2&gt;

&lt;p&gt;With the event store in place, the complete flow for a command becomes explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;CommandGateway&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;EventStore&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gw&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;CommandGateway&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aggregateID&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="n"&gt;CommandHandler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// 1. Load events&lt;/span&gt;
    &lt;span class="n"&gt;storedEvents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aggregateID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"load aggregate: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// 2. Reconstruct state&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;Replay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;storedEvents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;currentVersion&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;storedEvents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// 3. Execute the handler (pure, no side effects)&lt;/span&gt;
    &lt;span class="n"&gt;newEvents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// 4. Persist the new events&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aggregateID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AggregateType&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;currentVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newEvents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If two commands arrive simultaneously on the same aggregate, the second fails with &lt;code&gt;ErrConcurrencyConflict&lt;/code&gt;. The client can retry — it will reload the aggregate including the events from the first command, recalculate the state, and re-execute the handler on the updated state. In practice, real conflicts are rare on well-bounded aggregates; most concurrent operations touch different aggregates.&lt;/p&gt;

&lt;p&gt;This is also the pattern that makes command handlers testable without infrastructure (Part 2): the gateway injects the state, the handler produces events, the gateway persists. Each responsibility is isolated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance — when events accumulate
&lt;/h2&gt;

&lt;p&gt;An aggregate with a few dozen events reloads in microseconds. An aggregate with 10,000 events — a long-lived order, a very active user account — starts to weigh on the replay. The solution is snapshots.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;es_snapshots&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;aggregate_id&lt;/span&gt;   &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;aggregate_type&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;version&lt;/span&gt;        &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;state_data&lt;/span&gt;     &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;     &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A snapshot is a captured, serialized state at a given point in time, associated with its version. Loading with a snapshot proceeds in two steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Load the most recent snapshot for the aggregate (if it exists)&lt;/li&gt;
&lt;li&gt;  Load only the events after the snapshot's version&lt;/li&gt;
&lt;li&gt;  Replay the partial events on the snapshot's state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The simplest practical rule: create a snapshot every 100 events. Most aggregates never reach this limit — a typical e-commerce aggregate generates 5 to 20 events over its lifetime. Snapshots are only necessary for high-volume aggregates: accounts, wallets, inventories updated hundreds of times per day.&lt;/p&gt;

&lt;p&gt;Important: snapshots are an optimization, not a change to the data model. Events remain the source of truth. A corrupted or deleted snapshot is not data loss — the aggregate can be reconstituted from the beginning of its events.&lt;/p&gt;

&lt;h2&gt;
  
  
  Series summary
&lt;/h2&gt;

&lt;p&gt;Across four parts, we built a complete CQRS implementation in Go:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Part 1 — The aggregate&lt;/strong&gt;: immutable state, &lt;code&gt;Transition()&lt;/code&gt; for applying events, &lt;code&gt;Clone()&lt;/code&gt; for dry-runs. The foundation that makes everything else testable.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Part 2 — Command handlers&lt;/strong&gt;: pure functions that take a state and a command, return events or an error. No database, no HTTP, testable in complete isolation.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Part 3 — Sagas and choreography&lt;/strong&gt;: coordination between aggregates through published events. Each service reacts to what interests it, without direct coupling. Sagas handle compensation in case of distributed failure.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Part 4 — The event store&lt;/strong&gt;: PostgreSQL as the persistence infrastructure. Append-only table, optimistic locking through UNIQUE constraint, subscriptions for projectors, LISTEN/NOTIFY for reactivity, snapshots for large-scale performance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These four patterns combine: the command gateway loads via the event store, calls the pure handler, persists the produced events. Subscribers read the global stream and maintain read views. Sagas listen for certain event types and trigger commands on other aggregates.&lt;/p&gt;

&lt;p&gt;The result is a system that is auditable by construction — the complete history is in the events —, testable at every layer, and operable with the PostgreSQL infrastructure you already manage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📄 Associated CLAUDE.md&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.web-developpeur.com/blog/claude-md/view.php?ctx=cqrs-event-sourcing-go" rel="noopener noreferrer"&gt;View&lt;/a&gt; • &lt;a href="https://www.web-developpeur.com/blog/claude-md/contexts/cqrs-event-sourcing-go.md" rel="noopener noreferrer"&gt;Download&lt;/a&gt; • &lt;a href="https://www.web-developpeur.com/blog/claude-md/" rel="noopener noreferrer"&gt;Catalogue&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cqrs</category>
      <category>eventsourcing</category>
      <category>go</category>
      <category>postgres</category>
    </item>
    <item>
      <title>10 AI Tools Every Developer Should Try in 2026</title>
      <dc:creator>Vasyl Popovych</dc:creator>
      <pubDate>Thu, 23 Apr 2026 08:56:09 +0000</pubDate>
      <link>https://forem.com/vasyl_popovych_37a71efb3d/10-ai-tools-every-developer-should-try-in-2026-2d7l</link>
      <guid>https://forem.com/vasyl_popovych_37a71efb3d/10-ai-tools-every-developer-should-try-in-2026-2d7l</guid>
      <description>&lt;p&gt;The reality of modern development is brutal. You open your laptop at 9 AM with a clear plan: finish that feature by lunch. But first, there's a standup meeting that runs long. Then Slack explodes with questions about yesterday's deployment. Your IDE throws a cryptic error that sends you down a Stack Overflow rabbit hole. By noon, you haven't written a single line of production code.&lt;/p&gt;

&lt;p&gt;According to &lt;a href="https://survey.stackoverflow.co/2024" rel="noopener noreferrer"&gt;Stack Overflow's 2024 Developer Survey&lt;/a&gt;, which polled over 65,000 developers worldwide, the average developer spends just 3.5 hours per day actually writing code. The rest vanishes into meetings, code reviews, documentation, debugging, and what one survey respondent called "context-switching hell." A study from the University of California Irvine found that it takes 23 minutes to fully regain focus after an interruption. With the average developer facing 12+ interruptions daily, that's four hours of productive time lost before you even factor in the work itself.&lt;/p&gt;

&lt;p&gt;The problem isn't that developers are inefficient. It's that we're drowning in overhead. Email hasn't fundamentally changed since the 1990s. Project management tools are basically digital sticky notes. We generate more data than we can analyze and create more tasks than we can possibly complete.&lt;/p&gt;

&lt;p&gt;But here's the shift: AI tools have finally moved beyond the hype cycle into genuinely useful territory. Not the kind that promises to replace developers (spoiler: that's not happening), but tools that eliminate the friction points eating your day. According to &lt;a href="https://github.blog/news-insights/research/research-quantifying-github-copilots-impact-on-developer-productivity-and-happiness/" rel="noopener noreferrer"&gt;GitHub's research&lt;/a&gt;, developers using AI coding assistants complete tasks 55% faster in controlled experiments. Stack Overflow's 2024 survey shows 76% of developers are now using or planning to use AI tools, up from 70% the previous year.&lt;/p&gt;

&lt;p&gt;More importantly, 82% of developers currently use AI tools for writing code, while 68% use them for searching for answers. The tools below aren't theoretical. They're battle-tested solutions that developers are using right now to reclaim their time and focus on work that actually matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. GitHub Copilot: The AI Pair Programmer That Actually Delivers
&lt;/h2&gt;

&lt;p&gt;GitHub Copilot has become the most widely adopted AI coding assistant, and the statistics back up the hype. As of early 2025, Copilot reached 20 million users with 4.7 million paid subscribers, according to multiple industry reports. More impressively, 90% of Fortune 100 companies have adopted it.&lt;/p&gt;

&lt;p&gt;The real story is in the productivity numbers. GitHub's own controlled research study found that developers using Copilot completed an HTTP server implementation in JavaScript 55% faster than those without it. Task completion time dropped from 2 hours and 41 minutes to just 1 hour and 11 minutes. Success rates improved from 70% to 78%.&lt;/p&gt;

&lt;p&gt;What makes Copilot genuinely useful isn't that it writes perfect code (it doesn't). It handles the boilerplate that nobody wants to write. Need to parse a CSV? Write unit tests? Set up API endpoints? Copilot generates the repetitive logic while you focus on architecture and business logic.&lt;/p&gt;

&lt;p&gt;According to data compiled from various enterprise deployments, Copilot now writes approximately 46% of code on average, reaching as high as 61% in Java projects. The acceptance rate for suggestions sits around 30%, meaning developers find roughly one in three suggestions valuable enough to use directly. Most importantly, 88% of Copilot-generated code stays in the final version after review.&lt;br&gt;
GitHub's research found that 73% of developers reported that Copilot helped them stay in flow state, and 87% said it preserved mental effort during repetitive tasks. When you're not fighting with boilerplate, you have more energy for the hard problems.&lt;/p&gt;

&lt;p&gt;Pricing is $10/month for individuals, $19/month for business users, and $39/month for enterprise. The individual plan is a no-brainer for freelancers. For teams, the business plan includes privacy guarantees that your code isn't used for training.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Cursor: The AI-Native Code Editor
&lt;/h2&gt;

&lt;p&gt;Cursor took a different approach than Copilot. Instead of being an extension, it's a complete IDE built around AI from the ground up. The results speak for themselves: Cursor became the fastest-growing SaaS product ever to reach $100 million in annual recurring revenue, hitting that milestone in just 12 months.&lt;/p&gt;

&lt;p&gt;By early 2025, Cursor had over 1 million users with 360,000 paying customers, achieved almost entirely through word-of-mouth. The company's valuation hit $2.6 billion in January 2025, with revenue projected to reach $200 million in 2025. More than half of Fortune 500 companies now use Cursor, according to company reports from mid-2025.&lt;/p&gt;

&lt;p&gt;What sets Cursor apart is context awareness across your entire codebase. You can ask it "where is user authentication handled?" and it points you to the relevant files. Tell it "add rate limiting to all API endpoints" and it makes consistent changes across multiple files. According to JetBrains' January 2026 AI Pulse survey, 18% of developers use Cursor at work, making it the second most popular AI coding tool after GitHub Copilot (29%).&lt;/p&gt;

&lt;p&gt;Enterprise users report remarkable results. Companies using Cursor see PR volume increase by over 25% and average PR size double, meaning they're shipping approximately 50% more code. One engineering manager noted that adoption in their organization grew from 150 to over 500 engineers (60% of the org) in just a few weeks.&lt;/p&gt;

&lt;p&gt;Enterprise users report remarkable results. Companies using Cursor see PR volume increase by over 25% and average PR size double, meaning they're shipping approximately 50% more code — a level of efficiency that mirrors how modern &lt;a href="https://www.instaservice.com/" rel="noopener noreferrer"&gt;home services platforms&lt;/a&gt; have streamlined on-demand work across industries. One engineering manager noted that adoption in their organization grew from 150 to over 500 engineers (60% of the org) in just a few weeks&lt;/p&gt;

&lt;p&gt;The editor feels like VS Code (it's built on the same foundation) but treats AI as a first-class feature. In head-to-head comparisons, 93% of engineers prefer Cursor over other AI coding tools, according to the company's enterprise data.&lt;/p&gt;

&lt;p&gt;Cursor offers a free tier for basic usage, with paid plans starting around $20/month. For teams serious about AI-assisted development, it's become the standard.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Stack Overflow for Teams: AI-Enhanced Knowledge Management
&lt;/h2&gt;

&lt;p&gt;Stack Overflow for Teams has evolved beyond Q&amp;amp;A into an AI-powered knowledge platform. With 84% of developers visiting Stack Overflow at least multiple times per month (many multiple times per day), according to their 2024 survey, it remains the most trusted developer resource.&lt;/p&gt;

&lt;p&gt;The 2024 survey revealed that 82% of developers learn to code with online resources, with Stack Overflow (80%) as one of the top resources alongside technical documentation (83%). More tellingly, 35% of developers report that some of their Stack Overflow visits result from AI-related issues, usually to verify or fix AI-generated code.&lt;/p&gt;

&lt;p&gt;Stack Overflow for Teams now integrates AI features that help organizations capture and surface institutional knowledge. Instead of answers scattered across Slack threads and outdated wikis, teams can build searchable, verified knowledge bases. The AI surfaces relevant internal discussions and documentation based on context, dramatically reducing time spent hunting for information.&lt;/p&gt;

&lt;p&gt;The platform serves over 20,000 organizations, from startups to Fortune 500 enterprises. For distributed teams where knowledge transfer is critical, Stack Overflow for Teams has become essential infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. ChatGPT: The Universal Coding Assistant
&lt;/h2&gt;

&lt;p&gt;ChatGPT remains the most-used AI tool among developers. According to Stack Overflow's 2024 survey, 82% of developers using AI search tools choose ChatGPT, making it far and away the market leader. GitHub Copilot comes in second at 41%, followed by Google Gemini at 24%.&lt;br&gt;
What makes ChatGPT valuable for developers isn't code generation (though it does that). It's the ability to quickly understand unfamiliar concepts, debug error messages, and explore different approaches to problems. Need to understand how WebSockets work? Want to compare different authentication strategies? Stuck on a cryptic compiler error? ChatGPT provides context and explanations that would take 30 minutes of reading documentation.&lt;/p&gt;

&lt;p&gt;The tool has 75% admiration among developers who use it, according to Stack Overflow's AI tool rankings. That's higher than most specialized coding tools, suggesting developers find genuine value despite the limitations.&lt;/p&gt;

&lt;p&gt;ChatGPT Plus ($20/month) unlocks GPT-4 and faster response times. For developers, the paid tier is worth it for complex technical questions where response quality matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Claude (by Anthropic): The Thoughtful AI Assistant
&lt;/h2&gt;

&lt;p&gt;Claude has carved out a niche as the AI assistant developers trust for complex reasoning and detailed explanations. While it doesn't dominate usage statistics like ChatGPT, Claude excels at tasks requiring nuanced understanding: architecture decisions, code refactoring, technical writing, and debugging subtle logic errors.&lt;br&gt;
Developers often use Claude when they need an AI that "thinks through" problems rather than just pattern-matching. It's particularly strong at explaining tradeoffs, identifying edge cases, and reviewing code with actual insight rather than generic suggestions.&lt;/p&gt;

&lt;p&gt;Claude Pro costs $20/month and includes higher usage limits and priority access during peak times. For developers who need detailed technical discussions, it's a strong complement to more code-focused tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Linear: Project Management That Doesn't Waste Time
&lt;/h2&gt;

&lt;p&gt;Linear has become the project management tool of choice for engineering teams who are tired of bloated alternatives. While not strictly an AI tool, Linear recently added AI features that automatically categorize issues, suggest labels, and identify related work.&lt;/p&gt;

&lt;p&gt;What makes Linear special is speed. The interface is keyboard-driven and blazingly fast. Creating issues, updating statuses, and organizing work takes seconds instead of minutes. For developers who context-switch frequently, this speed adds up to hours saved per week.&lt;/p&gt;

&lt;p&gt;Linear integrates with GitHub, Figma, and Slack, pulling in context automatically. When a bug is reported, Linear can surface related PRs, similar issues, and affected components without manual digging.&lt;br&gt;
Thousands of engineering teams use Linear, from early-stage startups to public companies. Pricing starts free for small teams, with paid plans from $8/month per user.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Notion AI: Documentation That Writes Itself
&lt;/h2&gt;

&lt;p&gt;Notion transformed from a note-taking app into a full knowledge management platform. With AI features added, it now handles one of developers' most hated tasks: documentation.&lt;/p&gt;

&lt;p&gt;Notion AI can summarize meeting notes, generate action items, create documentation from code comments and commit messages, and transform rough notes into polished technical specs. That feature you shipped six months ago that nobody documented? Feed Notion AI your Slack discussions and git history, and it generates a first draft.&lt;/p&gt;

&lt;p&gt;For teams, Notion AI excels at synthesis. Drop in notes from multiple planning meetings and ask it to create a unified project brief. It identifies overlaps, contradictions, and gaps better than manually rereading everything.&lt;/p&gt;

&lt;p&gt;According to their case studies, teams report 40% less time spent searching for information after implementing AI features. For developers, that's time redirected to building instead of hunting through documentation.&lt;/p&gt;

&lt;p&gt;Notion AI costs $10/month per user on top of regular Notion plans. For teams already using Notion, it's a natural extension.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Perplexity: The Developer-Friendly Search Engine
&lt;/h2&gt;

&lt;p&gt;Perplexity reimagines search for the AI era. Instead of a list of links, you get direct answers with citations. For developers researching new libraries, comparing frameworks, or troubleshooting issues, Perplexity saves the time spent clicking through search results.&lt;/p&gt;

&lt;p&gt;The tool excels at technical queries where you need current information. "What's the best way to handle authentication in Next.js 14?" returns a synthesized answer with links to official docs, blog posts, and GitHub discussions. You can follow up with questions to drill deeper without starting a new search.&lt;/p&gt;

&lt;p&gt;Perplexity Pro ($20/month) unlocks unlimited searches and access to more powerful AI models. For developers who do heavy research, it's worth the upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Raycast: The Smart Command Bar
&lt;/h2&gt;

&lt;p&gt;Raycast is a Mac-only productivity tool that replaces Spotlight with something far more powerful. It includes AI features that let you write code snippets, draft messages, and automate workflows without leaving your keyboard.&lt;/p&gt;

&lt;p&gt;For developers, Raycast shines at eliminating micro-tasks. Need to convert JSON to TypeScript interfaces? Encode a string to base64? Search your GitHub repos? Format a timestamp? All available instantly via keyboard shortcuts.&lt;/p&gt;

&lt;p&gt;Raycast integrates with dozens of developer tools: GitHub, Linear, Jira, Figma, VS Code. You can search issues, create PRs, and manage tasks without context-switching to different apps.&lt;/p&gt;

&lt;p&gt;Raycast is free with a Pro tier ($8/month) that includes AI features and unlimited cloud sync. For Mac users, it's an instant productivity boost.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Sourcegraph Cody: Code Search That Actually Understands Your Codebase
&lt;/h2&gt;

&lt;p&gt;Sourcegraph Cody is an AI coding assistant that specializes in understanding large codebases. Unlike general-purpose AI tools, Cody indexes your entire repository and understands the relationships between files, functions, and dependencies.&lt;/p&gt;

&lt;p&gt;Ask Cody "where do we handle user permissions?" and it shows you every relevant file, not just files with "permissions" in the name. Request "add logging to all database queries" and it identifies every query in your codebase and suggests consistent changes.&lt;/p&gt;

&lt;p&gt;For teams working on large projects, Cody solves the onboarding problem. New developers can ask questions about the architecture and get accurate answers instead of bothering senior engineers or guessing from incomplete documentation.&lt;/p&gt;

&lt;p&gt;Sourcegraph offers a free tier for individual developers, with team plans starting at $9/month per user. For companies with large codebases, the enterprise tier includes custom training on your specific code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trust Problem: Why Developers Are Skeptical
&lt;/h2&gt;

&lt;p&gt;Here's the uncomfortable reality: while AI adoption is skyrocketing, trust is falling. Stack Overflow's 2024 survey revealed that only 43% of developers trust the accuracy of AI tools, and 45% believe AI tools are bad or very bad at handling complex tasks.&lt;/p&gt;

&lt;p&gt;The biggest frustration, cited by 66% of developers in Stack Overflow's 2025 survey, is dealing with "AI solutions that are almost right, but not quite." This leads to the second-biggest complaint: debugging AI-generated code takes longer than writing it manually (45% of developers).&lt;/p&gt;

&lt;p&gt;This creates a paradox. Developers use AI tools daily (62% of professional developers in 2024), but 46% don't trust the output. The solution? Treat AI suggestions like suggestions from a junior developer: useful starting points that require review. AI handles the boilerplate, you handle the judgment calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making AI Tools Work for You
&lt;/h2&gt;

&lt;p&gt;Don't try to adopt all ten tools at once. Pick one that addresses your biggest pain point. Drowning in boilerplate code? Start with Copilot or Cursor. Spending too much time searching documentation? Try Perplexity. Documentation piling up? Add Notion AI.&lt;/p&gt;

&lt;p&gt;Use it consistently for two weeks. AI tools have a learning curve because they work differently than traditional software. They get better as they learn your patterns, and you get better at knowing when to use them.&lt;/p&gt;

&lt;p&gt;The developers succeeding in 2026 aren't using more tools. They're using the right tools for friction points in their workflow. The goal isn't to have the most AI assistants. It's to spend less time on overhead and more time solving real problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Will AI tools replace developers?&lt;/strong&gt;&lt;br&gt;
No. Stack Overflow's 2024 survey found that 70% of professional developers don't see AI as a threat to their jobs. AI tools excel at generating boilerplate and handling repetitive tasks, but they don't understand business requirements, make architectural decisions, or navigate organizational complexity. Every company adopting these tools is hiring more developers, not fewer, because they can finally tackle their backlogs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much do developers actually trust AI-generated code?&lt;/strong&gt;&lt;br&gt;
Not much, and trust is declining. Only 43% of developers trust AI tool accuracy according to Stack Overflow's 2024 survey, down from previous years. The key is treating AI code like any other code: review it carefully, test it thoroughly, and don't deploy it without human verification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do AI tools actually make developers more productive?&lt;/strong&gt;&lt;br&gt;
Yes, but with caveats. GitHub's research shows developers complete tasks 55% faster with Copilot in controlled experiments. However, some studies show contradictory results, with developers taking 19% longer when using AI tools in certain contexts. Productivity gains depend heavily on how developers integrate the tools and what types of tasks they're working on.&lt;/p&gt;

&lt;p&gt;What are the biggest challenges with AI coding tools?&lt;br&gt;
According to Stack Overflow's 2024 survey, the top challenges are: inaccurate code suggestions (66%), longer debugging times (45%), and poor suitability for complex tasks (45%). Developers also cite concerns about misinformation (79%) and lack of source attribution (65%) as ethical issues with AI tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How are junior developers affected by AI tools?&lt;/strong&gt;&lt;br&gt;
This is debated. Some worry that junior developers become too dependent on AI and don't develop fundamental skills. Others argue AI helps juniors learn faster by providing instant feedback and examples. The consensus is that juniors should understand the code AI generates, not just copy-paste it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which AI coding tool has the most users?&lt;/strong&gt;&lt;br&gt;
GitHub Copilot leads with 20 million total users and 4.7 million paid subscribers as of early 2025. It's used by 90% of Fortune 100 companies. Among AI search tools, ChatGPT dominates with 82% of developers using it, followed by GitHub Copilot (41%) and Google Gemini (24%), according to Stack Overflow's 2024 survey.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need to pay for AI tools or are free versions enough?&lt;/strong&gt;&lt;br&gt;
It depends on usage intensity. Most tools offer free tiers that work fine for occasional use. Heavy users benefit from paid plans: faster responses, higher usage limits, and access to better models. GitHub Copilot Individual ($10/month) and ChatGPT Plus ($20/month) are the most common paid subscriptions among developers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do AI tools handle proprietary code and privacy?&lt;/strong&gt;&lt;br&gt;
Enterprise versions of tools like GitHub Copilot and Cursor include privacy guarantees that your code isn't used for training models. They typically offer SOC 2 compliance, zero data retention agreements with AI providers, and private deployment options. Always check privacy policies and use business/enterprise plans if working with proprietary code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;AI tools won't make you a better developer by themselves. They'll make you a faster developer at the things that don't require judgment, creativity, or domain expertise. That's still valuable. Every hour you save on boilerplate, documentation, and debugging is an hour you can spend on architecture, problem-solving, and building features that matter.&lt;/p&gt;

&lt;p&gt;The statistics are clear: 76% of developers are using or planning to use AI tools, and that number keeps climbing. Tools like GitHub Copilot and Cursor have achieved mainstream adoption in under three years. The trend isn't reversing.&lt;/p&gt;

&lt;p&gt;The developers winning in 2026 aren't the ones writing more code. They're the ones shipping better products by letting AI handle the grunt work. Your competitors are probably already using some of these tools. The question isn't whether to adopt AI. The question is how much of your time you want to keep spending on tasks that could be automated.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Why use an AI gateway at all?</title>
      <dc:creator>lulu77-mm</dc:creator>
      <pubDate>Thu, 23 Apr 2026 08:55:01 +0000</pubDate>
      <link>https://forem.com/lulu77mm/why-use-an-ai-gateway-at-all-5clc</link>
      <guid>https://forem.com/lulu77mm/why-use-an-ai-gateway-at-all-5clc</guid>
      <description>&lt;p&gt;&lt;strong&gt;Before picking a platform, I think it's worth asking: why even bother with an aggregation layer?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For me, the pain point became obvious once I started juggling more than two model providers. Different API keys, different billing cycles, different request formats, and the constant context-switching between docs. If you're building anything that needs to switch between GPT for reasoning, Claude for coding, or a local model for cost-saving, a unified API is no longer a nice-to-have—it's a productivity multiplier. It abstracts away the boring plumbing so you can focus on what you're actually building.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenRouter — the established workhorse&lt;/strong&gt;&lt;br&gt;
Like many of you, I've used OpenRouter extensively. Its core value is clear: unmatched model breadth (300+ and counting), smart routing with fallbacks, and a massive community. It's the best discovery engine out there—want to test a newly released open-source model the day it drops? OpenRouter probably has it. The trade-off, which is transparently disclosed, is the platform fee baked into usage.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;BasicRouter.ai — a newer alternative I'm exploring&lt;/strong&gt;&lt;br&gt;
Recently I stumbled upon BasicRouter.ai. Model count is smaller (around 50 curated ones), but it covers my daily stack—GPT, Claude, Gemini, DeepSeek, Qwen—and notably includes native image and video generation endpoints (Kling, Jimeng, Qwen-image) that OpenRouter doesn't focus on. Pricing is direct (no markup), and there's a small credit to test the waters. It feels like a pragmatic, "just the models I actually use" alternative.&lt;/p&gt;

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

&lt;p&gt;**Curious what the rest of you are using to wrangle multiple models. **Sticking with direct provider APIs, or leaning on gateways? Let's hear it. 🦞&lt;/p&gt;

</description>
      <category>ai</category>
      <category>apigateway</category>
      <category>api</category>
      <category>agents</category>
    </item>
    <item>
      <title>The Dawn of a New Era: Google's Gemini Enterprise Agent’s Platform</title>
      <dc:creator>miracle eze</dc:creator>
      <pubDate>Thu, 23 Apr 2026 08:54:18 +0000</pubDate>
      <link>https://forem.com/miracle_eze_0300fa8c9a9e1/the-agentic-leap-how-black-innovations-africa-is-building-the-future-with-gemini-1ml3</link>
      <guid>https://forem.com/miracle_eze_0300fa8c9a9e1/the-agentic-leap-how-black-innovations-africa-is-building-the-future-with-gemini-1ml3</guid>
      <description>&lt;p&gt;The Agentic Leap: How Black Innovations Africa is Building the Future with Google’s Gemini Enterprise Agent Platform &lt;/p&gt;

&lt;p&gt;Africa is no longer just a participant in the global digital economy; it is becoming its architect. As the Google Cloud NEXT ‘26 announcements unfold, it’s clear that we have entered the Agentic Era. This isn’t just a moment of growth—it is a definitive stepping stone that positions Nigerian and African technology as a cornerstone of the global future.&lt;/p&gt;

&lt;p&gt;The Agentic Shift: Beyond Chatbots&lt;/p&gt;

&lt;p&gt;The breakthrough this year is Agentic AI. Unlike traditional AI that simply processes data, Agentic AI—powered by Google Cloud’s Gemini 1.5 Pro and the new Agent Development Kit (ADK)—acts as a proactive partner. For Nigerian businesses, this means moving from "automation" to "autonomy."&lt;br&gt;
Black Innovations Africa: A Three-Pronged Revolution&lt;br&gt;
As one of the leaders in the Nigerian tech space, Black Innovations Africa is using this week’s announcements to accelerate our three core missions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;High-Velocity B2B Services&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We are leveraging the Agentic Data Cloud to transform B2B integrations. By using agentic "function calling," we’ve reduced the time it takes to sync disparate client systems by 60%. We aren't just giving clients a dashboard; we’re giving them a "digital taskforce" that executes multi-step business logic in real-time.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Building Proprietary Solutions with ADK&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We don't just use tools; we build them. Black Innovations Africa is currently developing internal platforms using Vertex AI Agent Builder.&lt;br&gt;
• The Goal: To create "Reasoning Agents" that can navigate local Nigerian market complexities—like fluctuating exchange rates or supply chain logistics—autonomously.&lt;br&gt;
• The Impact: This allows us to move from "concept" to "market-ready" in weeks instead of months, creating solutions that are uniquely African and globally competitive.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Training the "Agentic Taskforce"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The most vital part of our mission is training. We are officially integrating the Google Cloud ADK into our curriculum.&lt;br&gt;
• From Coders to Architects: We are teaching our students to move beyond basic syntax. They are learning to orchestrate multi-agent systems, manage Agentic SecOps, and use BigQuery Graph for supply chain traceability.&lt;br&gt;
• A New Baseline: By teaching the youth of Nigeria to build with Gemini today, we are ensuring they aren't just "job seekers"—they are the architects of the next billion-dollar startups.&lt;/p&gt;

&lt;p&gt;A Vision Realized: Prosperity Through Intelligence&lt;/p&gt;

&lt;p&gt;This evolution aligns with a broader vision: a world where African ingenuity is a primary export. Agentic AI makes our businesses faster, but more importantly, it makes them richer in capability. It encourages upcoming startups to dream bigger, knowing that with Google Cloud, a team in Lagos has the same power as a team in Silicon Valley.&lt;/p&gt;

&lt;p&gt;"The digital revolution in Africa is the single greatest opportunity for global advancement. At Black Innovation Africa, we are building the agents that will lead it."&lt;/p&gt;

&lt;p&gt;The Road Ahead&lt;/p&gt;

&lt;p&gt;As infrastructure improves and agentic workflows become the standard, the "stepping stone" of today will become the "gold standard" of tomorrow. Africa has arrived, the intelligence is agentic, and the world is finally tuning in.&lt;/p&gt;

</description>
      <category>cloudnextchallenge</category>
      <category>google</category>
      <category>googlecloud</category>
    </item>
    <item>
      <title>OCI Run Command Advanced Guide: Remote Execution, Object Storage Scripts, and Production Troubleshooting</title>
      <dc:creator>Bonthu Durga Prasad</dc:creator>
      <pubDate>Thu, 23 Apr 2026 08:53:24 +0000</pubDate>
      <link>https://forem.com/bonthu_durgaprasad_60725/oci-run-command-advanced-guide-remote-execution-object-storage-scripts-and-production-2687</link>
      <guid>https://forem.com/bonthu_durgaprasad_60725/oci-run-command-advanced-guide-remote-execution-object-storage-scripts-and-production-2687</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Managing remote servers usually means logging in through SSH (Linux) or RDP (Windows). While that works, it also means managing ports, credentials, and access controls.&lt;/p&gt;

&lt;p&gt;Oracle Cloud Infrastructure (OCI) offers a cleaner option called Run Command.&lt;/p&gt;

&lt;p&gt;OCI Run Command allows you to remotely execute commands or scripts on OCI Compute instances directly from the OCI Console, OCI CLI, or API — without logging in to the server manually.&lt;/p&gt;

&lt;p&gt;This blog explains what OCI Run Command is, how it works, what is required, common statuses, troubleshooting, and best practices.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is OCI Run Command?
&lt;/h2&gt;

&lt;p&gt;OCI Run Command is a feature that lets you run commands remotely on an OCI Compute instance using the Oracle Cloud Agent installed on that server.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;p&gt;hostname&lt;br&gt;
whoami&lt;br&gt;
systemctl restart nginx&lt;br&gt;
df -h&lt;/p&gt;

&lt;p&gt;Instead of connecting to the server manually, OCI sends the command securely through the cloud control plane.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Use Run Command?
&lt;/h2&gt;

&lt;p&gt;OCI Run Command is useful when you want to:&lt;/p&gt;

&lt;p&gt;Run quick administrative commands&lt;br&gt;
Restart services&lt;br&gt;
Collect logs&lt;br&gt;
Check disk space / memory&lt;br&gt;
Update configuration&lt;br&gt;
Run scripts remotely&lt;br&gt;
Troubleshoot servers without SSH/RDP access&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;Administrator&lt;br&gt;
     │&lt;br&gt;
     ▼&lt;br&gt;
OCI Console / OCI CLI&lt;br&gt;
     │&lt;br&gt;
     ▼&lt;br&gt;
Run Command Service&lt;br&gt;
     │&lt;br&gt;
     ▼&lt;br&gt;
OCI Instance Agent&lt;br&gt;
     │&lt;br&gt;
     ▼&lt;br&gt;
Compute Instance&lt;br&gt;
     │&lt;br&gt;
 ┌───┴──────────────┐&lt;br&gt;
 ▼                  ▼&lt;br&gt;
Object Storage      Log Files&lt;br&gt;
(Scripts)           (Output)&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Inline Commands vs Large Scripts
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Use Inline Commands For:&lt;/strong&gt;&lt;br&gt;
hostname&lt;br&gt;
uptime&lt;br&gt;
df -h&lt;br&gt;
systemctl status httpd&lt;br&gt;
&lt;strong&gt;Use Object Storage Scripts For:&lt;/strong&gt;&lt;br&gt;
Application deployment&lt;br&gt;
Package installation&lt;br&gt;
Multi-step patching&lt;br&gt;
Configuration enforcement&lt;br&gt;
Long shell logic&lt;/p&gt;

&lt;h2&gt;
  
  
  Main Components Required
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. OCI Compute Instance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Target server where command will run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Oracle Cloud Agent&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Installed on OCI instance. This agent communicates with OCI services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Run Command Plugin&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Must be enabled inside Oracle Cloud Agent settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. IAM Policies&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Permissions are needed for:&lt;/p&gt;

&lt;p&gt;User who creates command&lt;br&gt;
Instance that consumes command&lt;br&gt;
&lt;strong&gt;5. Dynamic Group&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Used to grant permissions to the OCI instance itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Enable Run Command Plugin&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Go to:&lt;/p&gt;

&lt;p&gt;OCI Console → Compute Instance → Oracle Cloud Agent&lt;/p&gt;

&lt;p&gt;Enable:&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Step 2: Create Dynamic Group&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Example rule:&lt;/p&gt;

&lt;p&gt;ALL {instance.compartment.id = ''}&lt;/p&gt;

&lt;p&gt;This means all servers in that compartment join the Dynamic Group.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Step 3: Dynamic Group Policies&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;p&gt;Allow dynamic-group DeployDG to use instance-agent-command-execution-family in compartment id &lt;/p&gt;

&lt;p&gt;Allow dynamic-group DeployDG to read instances in compartment id &lt;/p&gt;

&lt;p&gt;Allow dynamic-group DeployDG to read buckets in compartment id &lt;/p&gt;

&lt;p&gt;Allow dynamic-group DeployDG to read objects in compartment id  where target.bucket.name=''&lt;/p&gt;

&lt;p&gt;Allow dynamic-group DeployDG to manage objects in compartment id  where target.bucket.name=''&lt;/p&gt;

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

&lt;p&gt;This allows the server to receive and execute commands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: User IAM Policies&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;p&gt;Allow group Admins to manage instance-agent-command-family in compartment id &lt;/p&gt;

&lt;p&gt;Allow group Admins to read instance-agent-command-execution-family in compartment id &lt;/p&gt;

&lt;p&gt;Allow group Admins to inspect instances in compartment id &lt;/p&gt;

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

&lt;h2&gt;
  
  
  What is ocarun in OCI Run Command?
&lt;/h2&gt;

&lt;p&gt;ocarun is the local execution user/context used by OCI Run Command plugin on the compute instance.&lt;/p&gt;

&lt;p&gt;When you send a command through Oracle Cloud Infrastructure Run Command, the instance agent receives it and executes it using the Run Command plugin. On many OCI images/platform setups, that execution is associated with ocarun.&lt;/p&gt;

&lt;p&gt;-&amp;gt; If you run commands that required sudo privilages then you should provide ocarun user to the admin privilage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example Run Command (OCI CLI)
&lt;/h2&gt;

&lt;p&gt;oci instance-agent command create \&lt;br&gt;
--compartment-id  \&lt;br&gt;
--target '{"instanceId":""}' \&lt;br&gt;
--content '{&lt;br&gt;
  "source":{&lt;br&gt;
    "sourceType":"TEXT",&lt;br&gt;
    "text":"hostname"&lt;br&gt;
  }&lt;br&gt;
}' \&lt;br&gt;
--timeout-in-seconds 600 \&lt;br&gt;
--display-name test-hostname&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Common Status Values Explained
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;lifecycle-state&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Shows execution status.&lt;/p&gt;

&lt;p&gt;ACCEPTED&lt;br&gt;
Command created and queued.&lt;br&gt;
IN_PROGRESS&lt;br&gt;
Command is currently running.&lt;br&gt;
SUCCEEDED&lt;br&gt;
Command completed successfully.&lt;br&gt;
FAILED&lt;br&gt;
Command ran but failed.&lt;br&gt;
CANCELED&lt;br&gt;
Execution canceled.&lt;/p&gt;

&lt;p&gt;To check the delivery status &lt;/p&gt;

&lt;p&gt;oci instance-agent command-execution list --compartment-id ocid1.compartment.oc1..aaaaaaaagz4mern4sk46kbebwqzl6czdowlud7rop7ornezr7axx6ja5jfla --instance-id ocid1.instance.oc1.ap-mumbai-1.anrg6ljr7gqo7aacuco546smbzylzekybcfbvp2vz2ygvwgu52vf62zk7cma --all&lt;/p&gt;

&lt;p&gt;delivery-state&lt;/p&gt;

&lt;p&gt;Shows whether command reached the server.&lt;/p&gt;

&lt;p&gt;ACKED&lt;br&gt;
Server received the command.&lt;br&gt;
EXPIRED&lt;br&gt;
Command was never picked up before timeout window expired.&lt;/p&gt;

&lt;p&gt;Usually caused by:&lt;/p&gt;

&lt;p&gt;Wrong Dynamic Group&lt;br&gt;
Agent issue&lt;br&gt;
Network issue&lt;br&gt;
Permissions issue&lt;/p&gt;

&lt;p&gt;-&amp;gt; If you get delivery status ACKED and exit code =0 then your command / script successfully executed over the remote server.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  How to Troubleshoot
&lt;/h2&gt;

&lt;p&gt;Check Agent Status&lt;/p&gt;

&lt;p&gt;Linux:&lt;/p&gt;

&lt;p&gt;systemctl status oracle-cloud-agent&lt;br&gt;
Restart Agent&lt;br&gt;
systemctl restart oracle-cloud-agent&lt;br&gt;
View Logs&lt;br&gt;
tail -100 /var/log/oracle-cloud-agent/agent.log&lt;/p&gt;

&lt;h2&gt;
  
  
  Upload Detailed Logs to Object Storage
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;For long executions, store logs locally:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;/tmp/deploy_httpd.log&lt;/p&gt;

&lt;p&gt;Then upload logs back to bucket using automation.&lt;/p&gt;

&lt;p&gt;This enables:&lt;/p&gt;

&lt;p&gt;✔ Retention&lt;br&gt;
✔ Audit trail&lt;br&gt;
✔ Team review&lt;br&gt;
✔ Full debugging&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Best Practices
&lt;/h2&gt;

&lt;p&gt;✔ Use least privilege IAM policies&lt;br&gt;
✔ Restrict bucket access&lt;br&gt;
✔ Sign scripts / validate source&lt;br&gt;
✔ Avoid plaintext secrets in scripts&lt;br&gt;
✔ Rotate credentials&lt;/p&gt;

&lt;h2&gt;
  
  
  Run Command vs SSH
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;SSH&lt;/th&gt;
&lt;th&gt;OCI Run Command&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Needs inbound port&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual session&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API driven&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auditable&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Strong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scalable fleet ops&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Strong&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;OCI Run Command is a powerful feature for remote server administration. Once properly configured, it becomes one of the easiest and safest ways to execute commands on OCI instances without direct login access.&lt;/p&gt;

&lt;p&gt;If you manage OCI servers regularly, it is worth enabling and learning.&lt;/p&gt;

</description>
      <category>runcommand</category>
      <category>oci</category>
      <category>oracle</category>
      <category>sre</category>
    </item>
    <item>
      <title>I've built an Obsidian Vault for Novel Writing &amp; Worldbuilding</title>
      <dc:creator>Panos Sakalakis</dc:creator>
      <pubDate>Thu, 23 Apr 2026 08:53:20 +0000</pubDate>
      <link>https://forem.com/sakalakis/ive-built-an-obsidian-vault-for-novel-writing-worldbuilding-59am</link>
      <guid>https://forem.com/sakalakis/ive-built-an-obsidian-vault-for-novel-writing-worldbuilding-59am</guid>
      <description>&lt;p&gt;For the past two and a half years, I've been writing a sci-fi novel. One of the reasons it took me so much time and I haven't even published the first book of the series, was finding the right place to store and organize all my thoughts and ideas.&lt;/p&gt;

&lt;p&gt;Like most beginner authors, I started with a blank document on Google Docs. But as I kept writing the book and the story progressed, everything from the characters and locations to events and dates grew to a point where I found myself looking at a document with hundreds of comments and notes. Unlike other writers, my mind can't work like that, and looking at my document was enough to make me procrastinate and pause the writing for a few weeks.&lt;/p&gt;

&lt;p&gt;I started looking for apps that I could use for worldbuilding and novel outlining, and I found quite a few options. First, the king: Scrivener. This is what most professional career authors recommend for every beginner, and after a good week of using it (that's how long their free trial gives you), I knew that Scrivener wasn't it. Don't get me wrong, it's pretty good and powerful and all, but not as customizable as I wanted it to be. I wanted something simpler, yet as powerful as Scrivener, but minimal enough to reduce the chaos I felt I was living with my novel.&lt;/p&gt;

&lt;p&gt;First, I tried creating my own software, a native app that fully supported Markdown, and it could have everything I wanted, exactly how I wanted it to be. But as many programmers/developers know, creating, maintaining, and making sure such a program actually works is far from easy. But it wasn't the coding part that I was afraid of, but how much time I'd have to invest in building it.&lt;/p&gt;

&lt;p&gt;Time was in essence, because I had just switched my career and focused on becoming what I had always dreamed of: a full-time author. But the only thing that was stopping me was me, unable to find a way to store my thoughts and ideas that my mind would appreciate. Because, you know, if it doesn't, I don't get those beautiful creativity boosts that it gives me every day.&lt;/p&gt;

&lt;p&gt;That's when I found Obsidian, and let me tell you, it was love at first sight. Sure, it came with a learning curve, especially for me, who kept looking at their &lt;a href="https://docs.obsidian.md/Home" rel="noopener noreferrer"&gt;developer docs&lt;/a&gt;, but I knew it was the perfect app for what I needed.&lt;/p&gt;

&lt;p&gt;So I built my own Vault and put everything there. As time went by, I kept improving it and adding whatever I needed. I kept things simple, used a minimal theme, and made sure that everything was well-linked. And after a while, I thought of sharing it on my Medium.com blog for authors who may find it interesting.&lt;/p&gt;

&lt;p&gt;And my oh my, do they found it interesting.&lt;/p&gt;

&lt;p&gt;Currently, it has over 180+ downloads on Gumroad alone, and a 5-star rating from a total of 6 reviews. People sent me a lot of feedback, and I've already released two versions of it. I'll continue updating, and the next version is gonna be even better after all the emails I've gotten from people who downloaded it.&lt;/p&gt;

&lt;p&gt;It's also closing on making $100 in donations, which is something I never believed I'd see - and for which I'm so grateful, as it supports me to continue focusing on my dream.&lt;/p&gt;

&lt;p&gt;With that in mind, say hello to 'The Novelist', a free and open-source Vault for Obsidian that's designed for novel writing and worldbuilding. Although it wasn't designed for non-fiction writers, you can easily adjust it to your needs.&lt;/p&gt;

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

&lt;p&gt;Here's everything you need to know about this Vault.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Worldbuilding with Canvas
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foumkrtg0ms2julf4qxzj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foumkrtg0ms2julf4qxzj.png" alt="The Novelist - Worldbuilding with Canvas" width="800" height="488"&gt;&lt;/a&gt;&lt;br&gt;
There's nothing special about this one if you've already been using it. Canvas is a built-in plugin that every Obsidian user can enable or disable, but few know how powerful it is for worldbuilding.&lt;/p&gt;

&lt;p&gt;This Canvas includes everything my book and story do, including the book's title and description, characters and locations overviews, ideas, notes, to-do lists, and everything else I feel like putting there. You can probably improve it and make it even better, as I did on my novel's vault, but for demo purposes, I kept it simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Colorful File Explorer
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl099gfzavmq3kpinrsb9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl099gfzavmq3kpinrsb9.png" alt="The Novelist - the colorful file explorer" width="800" height="626"&gt;&lt;/a&gt;&lt;br&gt;
Obsidian's 'File Explorer' is located on the left side, and it's the place where all your folders, files, and documents are located. I didn't want to have to scroll through hundreds of those, so I organized everything as simply as I could.&lt;/p&gt;

&lt;p&gt;The structure:&lt;/p&gt;

&lt;h3&gt;
  
  
  Chapters
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F974uzdmame2twnou3vyq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F974uzdmame2twnou3vyq.png" alt="The Novelist - All chapters organized" width="800" height="488"&gt;&lt;/a&gt;&lt;br&gt;
Includes a document called "All Chapters" and, just below it, you can find all the chapters separated individually (e.g, Chapter 1, Chapter 2, Chapter 3, and so on). I keep the 'All Chapters' document as my backup and move each chapter I fully complete there. At the end, it's a process of exporting/copying the whole document and importing it into Atticus, Vellum, etc.&lt;/p&gt;

&lt;h3&gt;
  
  
  Characters
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyckhliqdvelrk3rzskjg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyckhliqdvelrk3rzskjg.png" alt="The Novelist - All characters organized" width="800" height="488"&gt;&lt;/a&gt;&lt;br&gt;
This is where your characters are located. If you have just a few characters, you can have them all inside this folder, or organize them into folders if you have lots of them. Each character has a few details that you can fill in, such as name, nickname, gender, role, age, family members, friendships, occupation, and so on, but you can remove/rename every property or add more. I've also written about their backgrounds, what kind of people they are, their biggest secrets and fears, and so much more.&lt;/p&gt;

&lt;h3&gt;
  
  
  Locations
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpygs0lw8v3czcq96vlc9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpygs0lw8v3czcq96vlc9.png" alt="The Novelist - All locations organized" width="800" height="488"&gt;&lt;/a&gt;&lt;br&gt;
The same as for characters, you can organize them in folders, and each location has its own properties, such as location name and type, how many citizens live there, and everything else that's needed for your story. I've also given a full background for each location, like when (and how) it was built, important events through its history, who's been running it, and everything I feel my story needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tab splitting
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvde4byczy0cwtd1imy1i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvde4byczy0cwtd1imy1i.png" alt="The Novelist - Splitting tabs in Obsidian" width="800" height="488"&gt;&lt;/a&gt;&lt;br&gt;
What I love about Obsidian is that it lets you split your tabs side by side, or even by placing them above or below your current tabs. This is perfect because while I'm writing for, let's say, a specific location or characters, I want another tab just beside it to see their backgrounds and all the details I've added for them. It's easy to forget some details when you're dealing with lots of different locations and characters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 'Graph View' that shows your connections
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwicp808fu2gy4nhdkh9s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwicp808fu2gy4nhdkh9s.png" alt="The Novelist - Obsidian's built-in Graph View" width="800" height="488"&gt;&lt;/a&gt;&lt;br&gt;
In the beginning, the 'Graph View' feature doesn't show a lot of information, and it's not as useful and powerful as it will become as your story progresses and you link everything together.&lt;/p&gt;

&lt;p&gt;Make sure to link your characters, locations, and everything else you want to be included in the 'Graph View', and after a while, you'll be able to see if you have any missing questions.&lt;/p&gt;

&lt;p&gt;Like any feature and tool, you can disable the 'Graph View' if you don't need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing Obsidian &amp;amp; The Novelist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Download and install the latest version of &lt;a href="https://obsidian.md/" rel="noopener noreferrer"&gt;Obsidian&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Download the latest version of The Novelist (&lt;a href="https://sakalakis.gumroad.com/l/the-novelist-free-obsidian-vault" rel="noopener noreferrer"&gt;Gumroad&lt;/a&gt; | &lt;a href="https://github.com/panossakalakis/the-novelist/releases" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Extract the &lt;strong&gt;the-novelist.zip&lt;/strong&gt; file (double-click on macOS or right-click and "Extract here" on Windows and Linux).&lt;/li&gt;
&lt;li&gt;Launch Obsidian and click on your Vault’s name in the bottom left corner, and click on “&lt;strong&gt;Manage vaults…&lt;/strong&gt;".&lt;/li&gt;
&lt;li&gt;In the “&lt;strong&gt;Open folder as vault&lt;/strong&gt;” section, click on the “Open” button and select the &lt;strong&gt;‘The Novelist’ folder&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Obsidian will ask you to &lt;strong&gt;"Enable community plugins and trust the author"&lt;/strong&gt;. Make sure to click on the &lt;strong&gt;"Trust"&lt;/strong&gt; button to load everything the vault has to offer.
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk7fxvuy6an3robjvrglr.png" alt="The Novelist - Trusting the author and plugins" width="800" height="564"&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Documentation
&lt;/h2&gt;

&lt;p&gt;If you don't know how to use Obsidian and The Novelist, please refer to the &lt;a href="https://panossakalakis.com/how-to-install-and-setup-the-novelist-vault/" rel="noopener noreferrer"&gt;Documentation page&lt;/a&gt;, where I share all its features and options with images included (and soon, videos).&lt;/p&gt;




&lt;p&gt;As always, I love getting feedback from people who use 'The Novelist', so don't hesitate to leave your comment or contact me with your ideas, bug reports, etc.&lt;/p&gt;

</description>
      <category>writing</category>
      <category>worldbuilding</category>
      <category>productivity</category>
      <category>obsidian</category>
    </item>
    <item>
      <title>Beyond the README: The Evolution of Markdown in the Age of Generative AI</title>
      <dc:creator>Jimit</dc:creator>
      <pubDate>Thu, 23 Apr 2026 08:48:31 +0000</pubDate>
      <link>https://forem.com/thejimit/beyond-the-readme-the-evolution-of-markdown-in-the-age-of-generative-ai-52ol</link>
      <guid>https://forem.com/thejimit/beyond-the-readme-the-evolution-of-markdown-in-the-age-of-generative-ai-52ol</guid>
      <description>&lt;p&gt;Markdown has been the undisputed king of developer communication for two decades. It’s the language of our READMEs, our static site generators (like Astro), and our technical blogs. &lt;/p&gt;

&lt;p&gt;But in 2026, Markdown is undergoing its most significant shift yet. It is no longer just a "formatting tool" for humans; it has become the &lt;strong&gt;fundamental interface between humans and Large Language Models (LLMs).&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhz1dirdu8zh9uf5obua1.png" alt="Markdown is King" width="800" height="443"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  1. Why Markdown Won the "Markup Wars"
&lt;/h2&gt;

&lt;p&gt;Before we look forward, we have to understand why Markdown beat out HTML, BBCode, and WikiText.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Portability:&lt;/strong&gt; A &lt;code&gt;.md&lt;/code&gt; file looks the same in VS Code, Obsidian, and GitHub.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Readability:&lt;/strong&gt; Unlike HTML, Markdown is human-readable even in its raw state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standardization:&lt;/strong&gt; With the rise of &lt;strong&gt;CommonMark&lt;/strong&gt; and &lt;strong&gt;GitHub Flavored Markdown (GFM)&lt;/strong&gt;, the "fragmentation" of the early 2000s has largely stabilized.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. The AI Pivot: Markdown as the "LLM Wire Format"
&lt;/h2&gt;

&lt;p&gt;If you’ve used ChatGPT, Claude, or Gemini, you’ve noticed they almost exclusively respond in Markdown. There is a technical reason for this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tokens and Structure:&lt;/strong&gt; LLMs are trained on massive amounts of web data. Markdown provides a low-token-cost way to define structure. While HTML tags like &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; consume many tokens, Markdown's &lt;code&gt;#&lt;/code&gt; and &lt;code&gt;*&lt;/code&gt; are incredibly efficient. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code-to-UI Mapping:&lt;/strong&gt;&lt;br&gt;
Markdown serves as the bridge for "UI-less" interfaces. When an AI generates a table in Markdown, it’s not just text; it’s a data structure that modern front-ends can instantly render into interactive components.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The Future: "Smart" Markdown and MDX 2.0
&lt;/h2&gt;

&lt;p&gt;We are moving toward a future where Markdown is &lt;strong&gt;executable.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  From Static to Dynamic (MDX)
&lt;/h3&gt;

&lt;p&gt;For those of us using frameworks like &lt;strong&gt;Astro&lt;/strong&gt; or &lt;strong&gt;Next.js&lt;/strong&gt;, MDX is already the standard. It allows us to import React/Preact components directly into our Markdown files. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The AI Future:&lt;/strong&gt; Imagine a Markdown file where an AI dynamically injects a live data chart based on the reader's local data—all defined within a standard &lt;code&gt;.md&lt;/code&gt; syntax.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Markdown as a Data Source (Content Collections)
&lt;/h3&gt;

&lt;p&gt;In 2026, we are seeing "Markdown-as-a-Database." Instead of complex SQL queries for blogs, we are using type-safe &lt;strong&gt;Content Collections&lt;/strong&gt; to treat folders of Markdown files as structured APIs. This makes it easier for AI agents to crawl, index, and update our documentation autonomously.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Best Practices for Markdown in 2026
&lt;/h2&gt;

&lt;p&gt;To ensure your Markdown is "Future-Proof" and "AI-Friendly," follow these standards:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Best Practice&lt;/th&gt;
&lt;th&gt;Why?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Frontmatter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Use YAML headers for metadata.&lt;/td&gt;
&lt;td&gt;Essential for SEO and Astro-style routing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Headers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stick to a strict hierarchy (H1 -&amp;gt; H2 -&amp;gt; H3).&lt;/td&gt;
&lt;td&gt;Helps LLMs understand document "chunks."&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Code Blocks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Always specify the language (e.g., &lt;code&gt;&lt;/code&gt;`&lt;code&gt;typescript&lt;/code&gt;).&lt;/td&gt;
&lt;td&gt;Enables syntax highlighting and AI code-parsing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Alt Text&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Never skip &lt;code&gt;![Alt text](url)&lt;/code&gt;.&lt;/td&gt;
&lt;td&gt;Critical for accessibility and AI image-recognition.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  5. Is Markdown Being Replaced?
&lt;/h2&gt;

&lt;p&gt;Short answer: &lt;strong&gt;No.&lt;/strong&gt; Markdown is becoming the "JSON of Content." While we might see new extensions (like &lt;strong&gt;Markdoc&lt;/strong&gt; from Stripe), the core syntax is too deeply embedded in our ecosystem to disappear.&lt;/p&gt;

&lt;p&gt;As we move toward "Vibe Coding" and AI-generated apps, Markdown will be the "source code" that humans review to ensure the AI stayed on track. It is the human-readable anchor in a world of machine-generated complexity.&lt;/p&gt;




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

&lt;p&gt;If you want to stay ahead as a developer, don't just "write" Markdown. Master &lt;strong&gt;MDX&lt;/strong&gt;, understand &lt;strong&gt;YAML frontmatter&lt;/strong&gt;, and learn how to structure your docs so they are easily digestible by both your peers and your AI collaborators.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What’s your favorite Markdown extension?&lt;/strong&gt; Are you Team Obsidian, or do you still do everything in a simple VS Code window? Let's discuss below!&lt;/p&gt;

</description>
      <category>markdown</category>
      <category>ai</category>
      <category>webdev</category>
      <category>documentation</category>
    </item>
    <item>
      <title>Compact Is Not TypeScript. That's the Whole Point.</title>
      <dc:creator>Tushar Pamnani</dc:creator>
      <pubDate>Thu, 23 Apr 2026 08:44:50 +0000</pubDate>
      <link>https://forem.com/midnight-aliit/compact-is-not-typescript-thats-the-whole-point-3d9</link>
      <guid>https://forem.com/midnight-aliit/compact-is-not-typescript-thats-the-whole-point-3d9</guid>
      <description>&lt;p&gt;Most developers approach Compact with the wrong frame.&lt;/p&gt;

&lt;p&gt;They see the syntax - functions, types, imports, curly braces, and conclude: &lt;em&gt;this is basically TypeScript with some ZK stuff sprinkled in.&lt;/em&gt; Then they start writing. And things break in ways that don't make sense. Loops that should work don't compile. Logic that feels correct fails silently. Private data bleeds into places it shouldn't.&lt;/p&gt;

&lt;p&gt;The syntax didn't lie to them. Their mental model did.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Illusion Is the Lesson
&lt;/h2&gt;

&lt;p&gt;Here's a Compact circuit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export circuit get(): Uint&amp;lt;64&amp;gt; {
  assert(state == State.SET, "Value not set");
  return value;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your brain reads this and fires the usual pattern: &lt;em&gt;function, assertion, return value.&lt;/em&gt; It looks like a getter. Call it, get a thing back.&lt;/p&gt;

&lt;p&gt;But that's not what's happening.&lt;/p&gt;

&lt;p&gt;This circuit doesn't "run." It declares a set of constraints. When you call it from your dApp, the proof system generates a zero-knowledge proof that those constraints were satisfied, without revealing the inputs. What goes on-chain isn't the output. It's the proof.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The frame shift:&lt;/strong&gt; You're not writing code that executes. You're writing a description of a valid state, and the system proves you were in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Mental Models. One Will Break You.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Traditional code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input → Code executes → Output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You write instructions. The machine follows them. You get a result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compact circuits:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input → Constraints are declared → Proof is generated → Proof is verified
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are no "instructions" in the traditional sense. The circuit defines relationships. The prover proves the relationships held. The chain verifies the proof without seeing the inputs.&lt;/p&gt;

&lt;p&gt;Nothing is executed. Correctness is proved.&lt;/p&gt;

&lt;p&gt;This is why &lt;code&gt;assert&lt;/code&gt; is your only runtime guard. It's not just input validation, it's the mechanism by which you define what a valid state even is. Every &lt;code&gt;assert&lt;/code&gt; is a constraint. Every constraint becomes part of the circuit. The circuit is the proof.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Constraints Are the Feature
&lt;/h2&gt;

&lt;p&gt;The first reaction most developers have to Compact's restrictions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No recursion&lt;/li&gt;
&lt;li&gt;Bounded loops only (bounds must be compile-time constants)&lt;/li&gt;
&lt;li&gt;Fixed type sizes&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;any&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Unsigned types only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;"These seem arbitrary. Why can't I just-"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;They're not arbitrary. They're causal.&lt;/p&gt;

&lt;p&gt;ZK proofs require finite circuits. A circuit is a fixed structure; gates, wires, constraints, determined entirely at compile time. If you could write unbounded loops or recursion, the compiler couldn't produce a circuit. The circuit literally couldn't exist. The proof couldn't be generated.&lt;/p&gt;

&lt;p&gt;The constraints don't limit what Compact can do. They're what make the magic possible at all.&lt;/p&gt;

&lt;p&gt;Accept this, and the language makes complete sense. Fight it, and you'll spend weeks trying to port TypeScript patterns into a tool designed around a completely different model.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "Privacy" Actually Means Here
&lt;/h2&gt;

&lt;p&gt;Compact is not just "TypeScript with encryption." The privacy model is structural.&lt;/p&gt;

&lt;p&gt;Midnight has two worlds:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;World&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;Who can see it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Public&lt;/td&gt;
&lt;td&gt;On-chain&lt;/td&gt;
&lt;td&gt;Everyone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private&lt;/td&gt;
&lt;td&gt;Local&lt;/td&gt;
&lt;td&gt;Only you&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Your sensitive data; balances, credentials, secrets, never leaves your machine. What goes on-chain is a proof that you ran the computation correctly on that private data. Validators verify the proof. They never see the inputs.&lt;/p&gt;

&lt;p&gt;This has a concrete implication: &lt;strong&gt;you have to know the boundary.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;export ledger&lt;/code&gt; is public. &lt;code&gt;witness&lt;/code&gt; data is private. &lt;code&gt;disclose()&lt;/code&gt; is the explicit line you cross when you intentionally move private data into public state.&lt;/p&gt;

&lt;p&gt;The compiler enforces this. If private data tries to flow into public state without going through &lt;code&gt;disclose()&lt;/code&gt;, it fails. But knowing &lt;em&gt;why&lt;/em&gt; this boundary exists — not just that it does — is what lets you design contracts correctly from the start instead of debugging your way there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Use Cases This Unlocks
&lt;/h2&gt;

&lt;p&gt;This matters because entire categories of blockchain applications were previously impossible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Healthcare:&lt;/strong&gt; Prove you meet age or eligibility requirements without revealing your date of birth or medical history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finance:&lt;/strong&gt; Prove your balance exceeds a threshold for a loan without revealing the balance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity:&lt;/strong&gt; Prove citizenship or credential validity without revealing the underlying document.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Competitive markets:&lt;/strong&gt; Submit bids or inventory levels that can be verified as legitimate without being revealed to competitors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't niche edge cases. They're the reason regulated industries haven't fully adopted public blockchains. With Compact, the privacy model that makes these use cases viable is built into the language, not bolted on after the fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Comparison Breaks Down
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;TypeScript&lt;/th&gt;
&lt;th&gt;Solidity&lt;/th&gt;
&lt;th&gt;Compact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Private data&lt;/td&gt;
&lt;td&gt;Convention only&lt;/td&gt;
&lt;td&gt;On-chain (visible)&lt;/td&gt;
&lt;td&gt;Stays local, always&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Functions&lt;/td&gt;
&lt;td&gt;Execute instructions&lt;/td&gt;
&lt;td&gt;Execute instructions&lt;/td&gt;
&lt;td&gt;Declare constraints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loops&lt;/td&gt;
&lt;td&gt;Unbounded&lt;/td&gt;
&lt;td&gt;Unbounded (gas-limited)&lt;/td&gt;
&lt;td&gt;Bounded at compile time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recursion&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;td&gt;Not allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privacy enforcement&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Type system + compiler&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The closest thing Compact resembles isn't TypeScript. It's a constraint satisfaction system with TypeScript syntax. The syntax is a DX choice, not a design philosophy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The One Thing to Take Away
&lt;/h2&gt;

&lt;p&gt;Every concept in Compact, witnesses, circuits, &lt;code&gt;disclose()&lt;/code&gt;, the boundedness rules, becomes clear once you have the right frame:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You're not writing programs. You're describing valid states and proving you were in them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With that frame:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Witnesses make sense: they're the private inputs the prover uses to generate the proof.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;disclose()&lt;/code&gt; makes sense: it's the intentional act of moving a constraint into public view.&lt;/li&gt;
&lt;li&gt;Bounded loops make sense: the circuit is fixed at compile time, so everything has to be fixed at compile time.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;assert&lt;/code&gt; makes sense: it's not validation, it's constraint declaration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without that frame, everything in Compact feels like TypeScript with arbitrary restrictions. With it, everything is exactly as it should be.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built for This
&lt;/h2&gt;

&lt;p&gt;Most documentation explains the &lt;em&gt;what&lt;/em&gt;. Compact's own docs are good. But the gap isn't information, it's the conceptual layer underneath.&lt;/p&gt;

&lt;p&gt;I've been building a structured guide that goes through each concept with the execution model shift front and center: &lt;strong&gt;&lt;a href="https://github.com/tusharpamnani/Compact-Book" rel="noopener noreferrer"&gt;Compact Book&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're trying to actually understand Compact rather than just cargo-cult your way through examples, start there.&lt;/p&gt;

&lt;p&gt;The syntax is the easy part. The mental model is the work. Get that right, and the rest follows.&lt;/p&gt;

</description>
      <category>midnight</category>
      <category>compact</category>
      <category>web3</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>SVG to PNG: Complete Guide for Developers [2026]</title>
      <dc:creator>Luca Sammarco</dc:creator>
      <pubDate>Thu, 23 Apr 2026 08:44:40 +0000</pubDate>
      <link>https://forem.com/samma1997/svg-to-png-complete-guide-for-developers-2026-261c</link>
      <guid>https://forem.com/samma1997/svg-to-png-complete-guide-for-developers-2026-261c</guid>
      <description>&lt;p&gt;When you actually need PNG instead of SVG&lt;/p&gt;

&lt;p&gt;SVG is the best format for most web content. A vector icon at 2 kilobytes renders pixel-perfect at any zoom level, theme-able with CSS, manipulable with JavaScript, and indexable by search engines if you add title and desc. For buttons, logos, illustrations, and data visualizations, SVG beats PNG on every axis.&lt;/p&gt;

&lt;p&gt;But there are five places where SVG does not work and you need PNG. First: app icons. iOS and Google Play require fixed-size PNGs — 1024 by 1024 masters with many derivative sizes. Submit SVG and get rejected.&lt;/p&gt;

&lt;p&gt;Second: favicon fallback. Modern browsers accept SVG favicons but Safari on older iOS versions and many email clients that preview favicons do not. Multi-size ICO is the safe fallback.&lt;/p&gt;

&lt;p&gt;Third: email templates. Gmail, Outlook, Apple Mail strip SVG from HTML. The only reliable inline image format in email is PNG or JPG.&lt;/p&gt;

&lt;p&gt;Fourth: social uploads. Instagram, WhatsApp, Facebook, and most ad platforms reject SVG uploads. They want raster.&lt;/p&gt;

&lt;p&gt;Fifth: print and embedded documents. Word, PowerPoint, many PDF pipelines, and print-on-demand services need raster. SVG may render unpredictably in Word specifically.&lt;/p&gt;

&lt;p&gt;viewBox math and intrinsic size&lt;/p&gt;

&lt;p&gt;The first thing a rasterizer needs to know is what size is this SVG naturally. There are three places it can find the answer, in priority order.&lt;/p&gt;

&lt;p&gt;One: the width and height attributes on the root SVG. Two: if only one is present, derive the other from the viewBox ratio. Three: if both are missing, fall back to the viewBox extent. Four: if nothing is declared, rasterizers pick a default — browsers use 300 by 150, SammaPix uses 512 by 512.&lt;/p&gt;

&lt;p&gt;Always declare both width and height on the root element. Skipping them leads to unpredictable output across rasterizers. This is the single most common reason that my SVG converted fine in tool A but broke in tool B — the two tools used different fallback rules.&lt;/p&gt;

&lt;p&gt;Scaling strategy: 1x, 2x, 3x, custom&lt;/p&gt;

&lt;p&gt;The output size is a function of intrinsic SVG size times scale factor. For a 100 by 100 SVG: 1x produces 100 by 100 PNG. 2x produces 200 by 200 PNG — standard for Retina and HiDPI. 3x produces 300 by 300 PNG — iPhone Plus and Pro models. 4x produces 400 by 400 PNG — Android XXXHDPI and 4K monitors. Custom allows any width up to 8192 pixels with height scaling to preserve aspect ratio.&lt;/p&gt;

&lt;p&gt;SammaPix SVG to PNG parses the intrinsic size from width, height, or viewBox automatically and lets you pick the scale preset or custom width. The maximum 8192 pixels is a deliberate guard — larger canvases can crash the tab on low-memory devices.&lt;/p&gt;

&lt;p&gt;Building a favicon pack from one SVG&lt;/p&gt;

&lt;p&gt;A modern favicon setup serves three files from one SVG master. First: favicon.svg — the original, served to modern browsers. Second: favicon.ico — multi-size container with 16, 32, 48 (and optionally 64, 128, 256) for legacy clients. Third: apple-touch-icon.png — 180 by 180 for iOS home screen shortcuts.&lt;/p&gt;

&lt;p&gt;The HTML is three link tags in the document head. Build the favicon.ico from your SVG using the SammaPix Favicon Generator — upload the SVG, pick sizes, download the ICO file. For the Apple touch icon, run the SVG through SVG to PNG at 180 pixels wide.&lt;/p&gt;

&lt;p&gt;App icons for iOS and Android&lt;/p&gt;

&lt;p&gt;Both iOS and Google Play require a 1024 by 1024 master PNG — no transparency for iOS App Store, transparency optional for Play. Historical derivative sizes have shrunk because both platforms now generate them from the master automatically, but some CI pipelines still expect the full matrix.&lt;/p&gt;

&lt;p&gt;iOS App Store: 1024 by 1024, no alpha, square. Google Play: 1024 by 1024, 32-bit PNG with alpha allowed. iOS home screen: 180 by 180 apple-touch-icon. Android adaptive: 512 by 512 foreground layer with safe-zone padding. PWA manifest: 192 and 512 declared in manifest.json.&lt;/p&gt;

&lt;p&gt;If you export a single 1024 PNG from SVG you can downscale inside Xcode or Android Studio. If you need all sizes pre-baked, SammaPix SVG to PNG custom width field accepts any value from 16 to 8192.&lt;/p&gt;

&lt;p&gt;Open Graph and social previews&lt;/p&gt;

&lt;p&gt;Social platforms require a raster image with a specific aspect ratio. 1200 by 630 for Facebook Open Graph standard. 1200 by 628 for Twitter summary_large_image. 1200 by 627 for LinkedIn recommendation.&lt;/p&gt;

&lt;p&gt;You cannot generate this directly from a square SVG — the aspect ratio mismatch means you either design a dedicated 1200 by 630 SVG or composite your logo inside a larger background. Do the layout in SVG first, then rasterize to PNG at custom width 1200.&lt;/p&gt;

&lt;p&gt;Transparency: keep it or flatten it&lt;/p&gt;

&lt;p&gt;PNG supports 8-bit alpha, same as SVG. By default the conversion preserves any transparent region. Two reasons to flatten to a solid background.&lt;/p&gt;

&lt;p&gt;First: iOS App Store. Submitted app icons must not have alpha channel. A 1024 PNG with transparency will be rejected.&lt;/p&gt;

&lt;p&gt;Second: email clients. Some legacy clients render transparent PNGs on a dark background, making light icons invisible. A solid white or theme-matched background avoids the issue.&lt;/p&gt;

&lt;p&gt;Print output: the DPI conversion&lt;/p&gt;

&lt;p&gt;Print pipelines do not think in pixels — they think in inches or millimetres at a specific DPI. The math is pixels width equals inches width times DPI.&lt;/p&gt;

&lt;p&gt;Common targets at 300 DPI offset printing standard. Business card 3.5 by 2 inches equals 1050 by 600 pixels. Postcard 6 by 4 inches equals 1800 by 1200 pixels. Letter 8.5 by 11 inches equals 2550 by 3300 pixels. A4 equals 2480 by 3508 pixels. Poster 24 by 36 inches equals 7200 by 10800 — exceeds the 8192 tool limit, split into quarters.&lt;/p&gt;

&lt;p&gt;Common pitfalls&lt;/p&gt;

&lt;p&gt;External fonts — if your SVG references a Google Font via import, the rasterizer cannot load it. Convert all text to paths before rasterizing, or embed the font with base64.&lt;/p&gt;

&lt;p&gt;External images — image xlink href to an external file will not load cross-origin. Inline via base64 or host same-origin.&lt;/p&gt;

&lt;p&gt;Script tags — rasterizers disable script inside SVG for security. Any JS-driven animation is gone in the PNG.&lt;/p&gt;

&lt;p&gt;Filters — complex SVG filters render differently across rasterizers. Test the output visually.&lt;/p&gt;

&lt;p&gt;Huge viewBox — an SVG with viewBox 0 0 10000 10000 scaled 4x produces a 40000 by 40000 PNG. Crash territory. Check intrinsic size before picking scale.&lt;/p&gt;

&lt;p&gt;Size benchmarks: SVG vs PNG at scale&lt;/p&gt;

&lt;p&gt;We took a 2.1 kilobyte logo SVG and rasterized at five target sizes. 256 by 256: PNG 14 kilobytes, 6.7 times the SVG. 512 by 512: PNG 38 kilobytes, 18 times. 1024 by 1024: PNG 112 kilobytes, 53 times. 2048 by 2048: PNG 320 kilobytes, 152 times. 4096 by 4096: PNG 920 kilobytes, 438 times.&lt;/p&gt;

&lt;p&gt;The SVG stays 2.1 kilobytes regardless of display size. The PNG grows quadratically because every additional pixel needs encoding. This is why you ship SVG to browsers and rasterize only when the target rejects SVG.&lt;/p&gt;

&lt;p&gt;Free browser-based SVG tools&lt;/p&gt;

&lt;p&gt;SammaPix ships three tools covering the full SVG workflow, all running locally in your browser. SVG to PNG for any size conversion. Favicon Generator for multi-size favicon ICO. Compress Images for post-conversion reduction.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;When do I actually need to convert SVG to PNG? When the destination does not accept SVG: iOS and Android app icons, favicon fallback for legacy browsers, email templates, social media uploads, print pipelines, Word and PDF embedding.&lt;/p&gt;

&lt;p&gt;What size PNG should I export from my SVG? Depends on the target. Favicons 16, 32, 48 minimum plus 64, 128, 256. App icons 1024 by 1024. Open Graph 1200 by 630. Retina web 2x the display size.&lt;/p&gt;

&lt;p&gt;How does the SVG viewBox affect the PNG output? The viewBox defines the SVG coordinate system. If your SVG has width and height attributes, that is the intrinsic size. A 4x scale produces 4x the pixels.&lt;/p&gt;

&lt;p&gt;Will transparency be preserved when I convert SVG to PNG? Yes by default. PNG supports 8-bit alpha, same as SVG.&lt;/p&gt;

&lt;p&gt;Why is my PNG file much larger than the SVG source? Because SVG encodes shapes as math and PNG stores every pixel. A 2 kilobyte SVG can render at any resolution.&lt;/p&gt;

&lt;p&gt;Can I batch convert SVG to PNG in the browser? Yes. SammaPix SVG to PNG runs 100 percent in your browser using the Canvas API. Drop up to 20 SVGs on the free plan, 200 on Pro.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.sammapix.com/blog/svg-to-png-complete-guide-developers" rel="noopener noreferrer"&gt;sammapix.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it free:&lt;/strong&gt; &lt;a href="https://www.sammapix.com" rel="noopener noreferrer"&gt;SammaPix&lt;/a&gt; — 35 browser-based image tools. Compress, resize, convert, remove background, and more. Everything runs in your browser, nothing uploaded.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>images</category>
      <category>tools</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
