<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ebrahim Samir</title>
    <description>The latest articles on DEV Community by Ebrahim Samir (@ebrahimsamir102).</description>
    <link>https://dev.to/ebrahimsamir102</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%2F3468874%2F246c5e5e-bd13-438e-a7ac-77a08ca11187.png</url>
      <title>DEV Community: Ebrahim Samir</title>
      <link>https://dev.to/ebrahimsamir102</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ebrahimsamir102"/>
    <language>en</language>
    <item>
      <title>Everything is a State Machine</title>
      <dc:creator>Ebrahim Samir</dc:creator>
      <pubDate>Tue, 28 Apr 2026 14:57:25 +0000</pubDate>
      <link>https://dev.to/ebrahimsamir102/everything-is-a-state-machine-1j4</link>
      <guid>https://dev.to/ebrahimsamir102/everything-is-a-state-machine-1j4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Every bug is a state you didn't account for.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What is a State
&lt;/h2&gt;

&lt;p&gt;A traffic light is always in exactly one situation: red, yellow, or green. It cannot be red and green simultaneously. At any moment it is one thing, and everything around it knows what to do because of that.&lt;/p&gt;

&lt;p&gt;That is a state: a specific, exclusive situation a system can be in, which determines what can happen next.&lt;/p&gt;

&lt;p&gt;The light also moves between states in defined ways. Green does not jump to red. The rules for moving between states are called transitions.&lt;/p&gt;

&lt;p&gt;A state machine is just this: a set of possible states, and the rules for moving between them.&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%2Fmermaid.ink%2Fimg%2Fc3RhdGVEaWFncmFtLXYyCiAgICBbKl0gLS0-IFJlZAogICAgUmVkIC0tPiBHcmVlbiA6IFRpbWVyCiAgICBHcmVlbiAtLT4gWWVsbG93IDogVGltZXIKICAgIFllbGxvdyAtLT4gUmVkIDogVGltZXI%3D" 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%2Fmermaid.ink%2Fimg%2Fc3RhdGVEaWFncmFtLXYyCiAgICBbKl0gLS0-IFJlZAogICAgUmVkIC0tPiBHcmVlbiA6IFRpbWVyCiAgICBHcmVlbiAtLT4gWWVsbG93IDogVGltZXIKICAgIFllbGxvdyAtLT4gUmVkIDogVGltZXI%3D" alt="Traffic light state diagram" width="152" height="348"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  It's Everywhere
&lt;/h2&gt;

&lt;p&gt;Once you see this shape, you find it in everything.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;button&lt;/strong&gt; has states: default, hover, loading, disabled, error. Clicking a loading button should do nothing. That is a transition rule.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;order&lt;/strong&gt; has states: draft, placed, confirmed, shipped, delivered, returned. You cannot ship an unconfirmed order. Also a transition rule.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;TCP connection&lt;/strong&gt; has states: closed, listen, syn-sent, established, time-wait. You cannot send data without the handshake completing. The state machine is the protocol.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;database&lt;/strong&gt; is a known state at any moment, changed only through explicit commands. The write-ahead log is the full history of every transition. Replay it from the beginning and you always arrive at the same result. That is why replication and point-in-time recovery are even possible.&lt;/p&gt;

&lt;p&gt;These are not metaphors. They are literally state machines, whether you modeled or think of them that way or not.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 1: Getting the Model Wrong
&lt;/h2&gt;

&lt;p&gt;Let's build a SaaS subscription system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Subscription&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsActive&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&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;Fine for now. Then a free trial gets added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsActive&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsTrial&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&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;Then payments start failing and you need a grace period. Then cancellations need to take effect at period end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsActive&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsTrial&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsPastDue&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsCancelled&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;CancellationDate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&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;Four booleans. Sixteen combinations. How many are valid? Maybe three.&lt;/p&gt;

&lt;p&gt;What does &lt;code&gt;IsActive=true, IsCancelled=true, IsPastDue=true&lt;/code&gt; mean? Nobody knows, but that row exists in your database right now.&lt;/p&gt;

&lt;p&gt;Each flag arrived with a legitimate feature request. The problem is not carelessness. The problem is that all four flags are trying to describe the same single thing: &lt;strong&gt;what situation is this subscription in right now?&lt;/strong&gt; That is a state. You cannot represent one thing with four independent booleans without eventually producing combinations that should never exist.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fix
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;SubscriptionStatus&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Trialing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;PastDue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Cancelled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Expired&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Subscription&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;SubscriptionStatus&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;TrialEndsAt&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;CurrentPeriodEndsAt&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&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;Impossible combinations are now impossible to represent. The type system enforces what reality already knew.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note on functional languages:&lt;/strong&gt; Languages with discriminated unions (F#, Rust, Haskell) go further. Data belonging to a &lt;code&gt;Cancelled&lt;/code&gt; state is literally inaccessible when the subscription is &lt;code&gt;Trialing&lt;/code&gt;. The bug moves from a runtime crash to a compile-time impossibility.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With the enum model, transitions become explicit decisions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Current State&lt;/th&gt;
&lt;th&gt;Target State&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Trialing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Active&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Trial ends, payment succeeds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Trialing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Expired&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Trial ends, no payment method&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Active&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PastDue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Payment fails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Active&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Cancelled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;User cancels&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PastDue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Active&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retry succeeds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PastDue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Expired&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Grace period ends&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cancelled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Active&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;User reactivates before period ends&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things become obvious when you write this out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every transition is a business decision.&lt;/strong&gt; Who decided a cancelled subscription can reactivate? What happens after 7 days past due? These questions existed before. The boolean model just let you avoid answering them. The state model forces you to answer them explicitly, where they belong.&lt;/p&gt;

&lt;p&gt;The rule is not "never use booleans." The rule is: &lt;strong&gt;invalid states should be impossible to represent, not just guarded against in code.&lt;/strong&gt; A model that achieves this is more honest. And that honesty compounds as the system grows.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 2: Getting the Execution Wrong
&lt;/h2&gt;

&lt;p&gt;Fixing the model solves representation. But even a correct model breaks if transitions are not durable.&lt;/p&gt;

&lt;p&gt;When a subscription moves to &lt;code&gt;PastDue&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;HandlePaymentFailed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;subscriptionId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;subscription&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSubscriptionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subscriptionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;subscription&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;SubscriptionStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PastDue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendPaymentFailedAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// what if this fails?&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_features&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RestrictAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// what if this fails?&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_crm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UpdateStatusAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"past_due"&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 DB write succeeds. Then the email service goes down. The method exits. Features never restricted. CRM never updated.&lt;/p&gt;

&lt;p&gt;You retry the whole method. Now the email fires twice.&lt;/p&gt;

&lt;p&gt;The problem: the transition happened, but its consequences did not. You need a way to say "this transition happened, and these things must eventually happen as a result, even if the process dies between now and then."&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fix: Ensuring Eventual Consistency of Side Effects
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;HandlePaymentFailed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;subscriptionId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;subscription&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSubscriptionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subscriptionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;subscription&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;SubscriptionStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PastDue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OutboxTask&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;new&lt;/span&gt; &lt;span class="nf"&gt;OutboxTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"send-payment-failed-email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OutboxTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"restrict-features"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OutboxTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"update-crm-past-due"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// atomic: both or neither&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A background worker picks up each task independently. If email is down, that task retries. The others are not affected. The subscription is already in the right state. Nothing runs twice.&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%2Fmermaid.ink%2Fimg%2Fc2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBEQiBhcyBEYXRhYmFzZQogICAgcGFydGljaXBhbnQgT3V0Ym94IGFzIE91dGJveCBUYWJsZQogICAgcGFydGljaXBhbnQgV29ya2VyIGFzIEJhY2tncm91bmQgV29ya2VyCiAgICBwYXJ0aWNpcGFudCBTZXJ2aWNlIGFzIEV4dGVybmFsIFNlcnZpY2UKICAgIERCLT4-T3V0Ym94OiBBdG9taWMgU2F2ZSAoU3RhdGUgKyBUYXNrcykKICAgIE5vdGUgb3ZlciBEQixPdXRib3g6IFNpbmdsZSBUcmFuc2FjdGlvbgogICAgV29ya2VyLT4-T3V0Ym94OiBQb2xsIFBlbmRpbmcgVGFza3MKICAgIFdvcmtlci0-PlNlcnZpY2U6IENhbGwgQVBJCiAgICBTZXJ2aWNlLS0-PldvcmtlcjogMjAwIE9LCiAgICBXb3JrZXItPj5PdXRib3g6IE1hcmsgQ29tcGxldGU%3D" 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%2Fmermaid.ink%2Fimg%2Fc2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBEQiBhcyBEYXRhYmFzZQogICAgcGFydGljaXBhbnQgT3V0Ym94IGFzIE91dGJveCBUYWJsZQogICAgcGFydGljaXBhbnQgV29ya2VyIGFzIEJhY2tncm91bmQgV29ya2VyCiAgICBwYXJ0aWNpcGFudCBTZXJ2aWNlIGFzIEV4dGVybmFsIFNlcnZpY2UKICAgIERCLT4-T3V0Ym94OiBBdG9taWMgU2F2ZSAoU3RhdGUgKyBUYXNrcykKICAgIE5vdGUgb3ZlciBEQixPdXRib3g6IFNpbmdsZSBUcmFuc2FjdGlvbgogICAgV29ya2VyLT4-T3V0Ym94OiBQb2xsIFBlbmRpbmcgVGFza3MKICAgIFdvcmtlci0-PlNlcnZpY2U6IENhbGwgQVBJCiAgICBTZXJ2aWNlLS0-PldvcmtlcjogMjAwIE9LCiAgICBXb3JrZXItPj5PdXRib3g6IE1hcmsgQ29tcGxldGU%3D" alt="Outbox pattern sequence diagram" width="903" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You did not arrive here by thinking "I need a queue." You arrived here by asking: &lt;strong&gt;what does it mean for a transition to be complete?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A transition is not complete when the status column changes. It is complete when all its consequences have been delivered.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Far You Need to Go
&lt;/h3&gt;

&lt;p&gt;The outbox pattern is one point on a spectrum. Where you land depends on what your system can afford to lose mid-transition:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DB transaction + background worker:&lt;/strong&gt; cheap, handles most cases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message broker:&lt;/strong&gt; consequences become independent services, each retrying on their own&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durable execution&lt;/strong&gt; (Temporal, Restate): entire workflows survive process failures and can resume at any point
What moves you along the spectrum is not ambition. It is the cost of losing a transition halfway through.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Problem 3: Migrating an Honest Model
&lt;/h2&gt;

&lt;p&gt;Modeling state correctly from the start is straightforward. The real tradeoff shows up when you need to change a model that is already in production.&lt;/p&gt;

&lt;p&gt;Adding a new state like &lt;code&gt;Paused&lt;/code&gt; to a 10-million-row subscriptions table is a migration. That migration has risk: downtime, locking, rollback complexity. The temptation is to add a quick &lt;code&gt;IsPaused&lt;/code&gt; boolean instead. No migration needed, ships today.&lt;/p&gt;

&lt;p&gt;Sometimes you take that shortcut to hit a deadline. Just call it what it is: you are trading a more honest model tomorrow for a faster ship today. The goal is not to never make this tradeoff. It is to make it consciously, understand the interest you are paying, and reduce how often it happens.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tools and Techniques Through This Lens
&lt;/h2&gt;

&lt;p&gt;Software has many dimensions to evaluate when choosing tools and techniques: performance, cost, operational complexity, team familiarity, and more. But once you have a state model, one useful lens is to look at tools and techniques through it. How do you need to represent state? What guarantees do your transitions require? What failures can you afford mid-transition? The answers narrow the space considerably.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool / Technique&lt;/th&gt;
&lt;th&gt;State problem it addresses&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Redis&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;State needs to outlive the process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Message broker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Transitions need to be durable and decoupled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Optimistic / pessimistic locking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Two actors must not transition the same state simultaneously&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Temporal / Restate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Workflows must survive infra failures mid-transition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Event sourcing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;State needs a complete, replayable history of every transition&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is not the only dimension worth considering. But it is a clarifying one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Your Model Must Survive New Requirements
&lt;/h2&gt;

&lt;p&gt;Every new requirement needs somewhere to land. It needs to attach to states that already exist, or introduce new ones cleanly. When your model honestly reflects reality, there is a clear place for new things to fit.&lt;/p&gt;

&lt;p&gt;When it does not, every change becomes harder than the one before. You end up building on top of a model that was already wrong, and the cost compounds quietly until it becomes expensive to change anything at all.&lt;/p&gt;

&lt;p&gt;Think of the subscription system. Once you have explicit states and transitions, a new requirement like "pause a subscription for up to 3 months" has a clear shape. You add a &lt;code&gt;Paused&lt;/code&gt; state, define its transitions, answer the business questions it raises, and ship it. The model absorbs it cleanly.&lt;/p&gt;




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

&lt;p&gt;One more thing worth naming: the same entity means different things in different parts of your system.&lt;/p&gt;

&lt;p&gt;A subscription in your billing service is not the same as a subscription in your feature flags service or your analytics service. Each context cares about different states and different transitions. Billing cares about &lt;code&gt;PastDue&lt;/code&gt; and &lt;code&gt;Expired&lt;/code&gt;. Feature flags care about &lt;code&gt;Active&lt;/code&gt; and &lt;code&gt;Cancelled&lt;/code&gt;. Analytics might not care about state at all, just the history of transitions.&lt;/p&gt;

&lt;p&gt;Trying to build one subscription model that serves all of them is how systems quietly become impossible to change. Model the entity as your part of the system actually sees it.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Take
&lt;/h2&gt;

&lt;p&gt;You cannot model the future. But the future grows from whatever you modeled today. The question is not whether your model is perfect. It is whether it is honest about what you know right now.&lt;/p&gt;

</description>
      <category>software</category>
    </item>
  </channel>
</rss>
