<?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: eboody</title>
    <description>The latest articles on Forem by eboody (@eran_dot_codes).</description>
    <link>https://forem.com/eran_dot_codes</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F640991%2Fb03073de-c812-4ae0-b4c8-b8084a9800e8.jpg</url>
      <title>Forem: eboody</title>
      <link>https://forem.com/eran_dot_codes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/eran_dot_codes"/>
    <language>en</language>
    <item>
      <title>Making Illegal Workflow States Unrepresentable in Rust</title>
      <dc:creator>eboody</dc:creator>
      <pubDate>Mon, 23 Mar 2026 20:31:07 +0000</pubDate>
      <link>https://forem.com/eran_dot_codes/making-illegal-workflow-states-unrepresentable-in-rust-3951</link>
      <guid>https://forem.com/eran_dot_codes/making-illegal-workflow-states-unrepresentable-in-rust-3951</guid>
      <description>&lt;p&gt;Rust developers already accept one important move: if something matters to correctness, it should be visible in the type system.&lt;/p&gt;

&lt;p&gt;That is what &lt;code&gt;Option&lt;/code&gt; does for absence. That is what &lt;code&gt;Result&lt;/code&gt; does for failure. They do not eliminate every bug, but they eliminate a specific class of bugs by making an important distinction impossible to ignore.&lt;/p&gt;

&lt;p&gt;Workflow state often deserves the same treatment.&lt;/p&gt;

&lt;p&gt;A draft is not the same thing as a published document. An unauthenticated connection is not the same thing as an authenticated one. A projected row from storage is not the same thing as a machine that has already proved it belongs to one legal state.&lt;/p&gt;

&lt;p&gt;And yet ordinary code often collapses those distinctions into a status enum, a few optional fields, and comments about what "should" be true.&lt;/p&gt;

&lt;p&gt;Statum exists to stop doing that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Goal: Representational Correctness
&lt;/h2&gt;

&lt;p&gt;Statum is a Rust typestate framework, but the more useful description is simpler: it is a tool for representational correctness.&lt;/p&gt;

&lt;p&gt;That is just a question of how accurately code models the thing it claims to model.&lt;/p&gt;

&lt;p&gt;If a workflow has legally different phases, those phases should not look like the same value plus conventions. They should look different in the type system too.&lt;/p&gt;

&lt;p&gt;A common model looks like this:&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="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Draft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;InReview&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Published&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Article&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;reviewer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&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;published_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;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;This is compact, but it can represent combinations that should never exist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a draft with &lt;code&gt;published_at&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;a published article with no publication data&lt;/li&gt;
&lt;li&gt;an in-review article with no reviewer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The type system cannot help much because all of those states have the same shape.&lt;/p&gt;

&lt;p&gt;Statum takes the opposite approach. If a state is legally different, it should be a different type.&lt;/p&gt;

&lt;h2&gt;
  
  
  What That Looks Like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;statum&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;machine&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;transition&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nd"&gt;#[state]&lt;/span&gt;
&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;ArticleState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Draft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;InReview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ReviewAssignment&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;Published&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PublishedReceipt&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;ReviewAssignment&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;reviewer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;PublishedReceipt&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;published_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[machine]&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Article&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ArticleState&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;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[transition]&lt;/span&gt;
&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;Article&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Draft&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;fn&lt;/span&gt; &lt;span class="nf"&gt;submit&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="n"&gt;reviewer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&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;Article&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;InReview&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;self&lt;/span&gt;&lt;span class="nf"&gt;.transition_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ReviewAssignment&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;reviewer&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="nd"&gt;#[transition]&lt;/span&gt;
&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;Article&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;InReview&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;fn&lt;/span&gt; &lt;span class="nf"&gt;approve&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="n"&gt;published_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&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;Article&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Published&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;self&lt;/span&gt;&lt;span class="nf"&gt;.transition_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PublishedReceipt&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;published_at&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;This changes the shape of the API in useful ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Article&amp;lt;Draft&amp;gt;&lt;/code&gt; and &lt;code&gt;Article&amp;lt;Published&amp;gt;&lt;/code&gt; are different types&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;submit()&lt;/code&gt; only exists where it is legal&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;approve()&lt;/code&gt; only exists where it is legal&lt;/li&gt;
&lt;li&gt;review data only exists during review&lt;/li&gt;
&lt;li&gt;publication data only exists after publication&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the real payoff. Legal states stop looking like raw data plus comments. They become distinct types with distinct operations and distinct valid data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Statum Fits
&lt;/h2&gt;

&lt;p&gt;Statum is a good fit when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;methods should only exist in some phases&lt;/li&gt;
&lt;li&gt;some data is valid in exactly one state&lt;/li&gt;
&lt;li&gt;transitions should be explicit&lt;/li&gt;
&lt;li&gt;correctness depends on distinguishing legal, illegal, and not-yet-validated states&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Statum is a bad fit when the state label is mostly descriptive and does not carry real behavioral or data-shape constraints. Not every status enum should be turned into typestate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Unusual Part: Typed Rehydration
&lt;/h2&gt;

&lt;p&gt;The most interesting part of Statum is what happens when the state does not start life as a freshly built machine.&lt;/p&gt;

&lt;p&gt;Real systems have database rows, append-only event streams, and projected snapshots. Those inputs are raw facts, not typed machines.&lt;/p&gt;

&lt;p&gt;Statum keeps that distinction sharp.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;#[validators]&lt;/code&gt;, a persisted type can prove which state it belongs to, and only then does it rebuild into a typed machine. Raw rows stay raw until they have earned that upgrade.&lt;/p&gt;

&lt;p&gt;That matters because the most dangerous bugs often happen at the boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deserializing persisted rows&lt;/li&gt;
&lt;li&gt;replaying events&lt;/li&gt;
&lt;li&gt;reconstructing workflows from storage&lt;/li&gt;
&lt;li&gt;assuming a status label is enough to trust the rest of the payload&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That boundary is why the rebuild API exists. Today the crate supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;into_machine()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;into_machines()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;into_machines_by(...)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;event projection helpers in &lt;code&gt;statum::projection&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule is simple: a projected row or event-derived snapshot should not become a typed workflow value until the data has proved which legal state it belongs to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mental Model
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#[state]      -&amp;gt; lifecycle phases
#[machine]    -&amp;gt; durable machine context
#[transition] -&amp;gt; legal edges
#[validators] -&amp;gt; typed rehydration from stored data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is most of the model.&lt;/p&gt;

&lt;p&gt;If the workflow shape fits, the API stays small and the safety payoff is real.&lt;/p&gt;

&lt;h2&gt;
  
  
  If You Want To Evaluate Statum
&lt;/h2&gt;

&lt;p&gt;These are the best entry points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/eboody/statum" rel="noopener noreferrer"&gt;https://github.com/eboody/statum&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://docs.rs/statum" rel="noopener noreferrer"&gt;https://docs.rs/statum&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Start here: &lt;a href="https://github.com/eboody/statum/blob/main/docs/start-here.md" rel="noopener noreferrer"&gt;https://github.com/eboody/statum/blob/main/docs/start-here.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Event-log case study: &lt;a href="https://github.com/eboody/statum/blob/main/docs/case-study-event-log-rebuild.md" rel="noopener noreferrer"&gt;https://github.com/eboody/statum/blob/main/docs/case-study-event-log-rebuild.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The question I would start with is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does this workflow have legal states that should be impossible to misrepresent in code?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is yes, typestate can be worth the extra explicitness.&lt;/p&gt;

&lt;p&gt;If you have built similar Rust workflows, I would be most interested in two things: where this feels simpler than handwritten typestate, and where it still feels heavier than it should.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>opensource</category>
      <category>statemachines</category>
    </item>
  </channel>
</rss>
