<?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: DimiDan</title>
    <description>The latest articles on DEV Community by DimiDan (@dimidan).</description>
    <link>https://dev.to/dimidan</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%2F579227%2F382e9002-f48c-4b04-9340-18a6685a3de3.png</url>
      <title>DEV Community: DimiDan</title>
      <link>https://dev.to/dimidan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dimidan"/>
    <language>en</language>
    <item>
      <title>Baking Accessibility into Your Design System: A Four-Layer Testing Strategy</title>
      <dc:creator>DimiDan</dc:creator>
      <pubDate>Wed, 12 Aug 2026 04:38:49 +0000</pubDate>
      <link>https://dev.to/dimidan/baking-accessibility-into-your-design-system-a-four-layer-testing-strategy-4c1o</link>
      <guid>https://dev.to/dimidan/baking-accessibility-into-your-design-system-a-four-layer-testing-strategy-4c1o</guid>
      <description>&lt;h2&gt;
  
  
  Why I care about Accessibility: a short pre-history
&lt;/h2&gt;

&lt;p&gt;Many years ago, at the jam session at friends's place, I met someone with a visual disability. You know standard small talk who works where... But when I mentioned I was a FE dev, the conversation changed instantly. I've been asked with &lt;em&gt;so many&lt;/em&gt; questions. Why does one site read perfectly with a screen reader while another is an unnavigable wall of "clickable, clickable, clickable"? Why does a form sometimes announce what a field is for, and sometimes just say "edit text"?&lt;/p&gt;

&lt;p&gt;But the question that stuck with me wasn't technical at all. This person asked how to explain to developers that accessibility attributes are so, so important, and how to pass a message to the dev community: &lt;strong&gt;&lt;em&gt;please don't ignore us&lt;/em&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I didn't have a good answer that evening. The honest one is uncomfortable: most developers don't ignore accessibility out of malice. They ignore it because nothing in their workflow ever tells them it's broken. Their linter is silent, their tests are green, their PR merges. The people affected are invisible in the feedback loop.&lt;/p&gt;

&lt;p&gt;What that conversation left me with is a simple thought I'd put on every developer's wall:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;As a dev, you can actually make someone's life better. Don't forget this.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We spend so much time on abstractions (bundle sizes, render cycles, type inference) that it's easy to forget the output is used by &lt;em&gt;people&lt;/em&gt;, and for some of them, one &lt;code&gt;aria-label&lt;/code&gt; is the difference between finishing a task independently and giving up. Very few professions get such a direct, low-cost way to improve someone's day.&lt;/p&gt;

&lt;p&gt;But intent alone doesn't scale: awareness fades, teams change, deadlines press. What scales is wiring that intent into the machinery: the linter, the test suite, the CI gate. When the tooling carries the message, remembering the users who aren't in the room stops depending on any individual's memory.&lt;/p&gt;

&lt;p&gt;Here's how we did that, concretely, in a design system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the design system is the leverage point
&lt;/h2&gt;

&lt;p&gt;Accessibility bugs are the most expensive bugs you'll ever ship. Not because they're hard to fix, but because of &lt;em&gt;where&lt;/em&gt; they live. A missing &lt;code&gt;aria-label&lt;/code&gt; in a product feature affects one screen. The same defect in a design system's &lt;code&gt;Button&lt;/code&gt; component affects every screen in every product that consumes it.&lt;/p&gt;

&lt;p&gt;That's the bad news. The good news is the inverse is also true: &lt;strong&gt;the design system is the single highest-leverage place to enforce accessibility&lt;/strong&gt;. Fix it once, and every consumer inherits the fix. Test it once, and every consumer inherits the guarantee.&lt;/p&gt;

&lt;p&gt;This post walks through a layered, automated accessibility testing strategy we implemented in a component library monorepo: the tooling, the code, the gotchas, and the rollout strategy that kept us from blocking every PR on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "just be careful" doesn't work
&lt;/h2&gt;

&lt;p&gt;Without a codified standard, every team makes its own calls on ARIA attributes, color contrast, focus management, and keyboard navigation. The results are predictably inconsistent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One team's modal traps focus; another's doesn't.&lt;/li&gt;
&lt;li&gt;One dropdown is a proper &lt;code&gt;listbox&lt;/code&gt;; another is a pile of clickable &lt;code&gt;div&lt;/code&gt;s.&lt;/li&gt;
&lt;li&gt;Contrast ratios drift as designers tweak tokens without running the numbers.
Beyond the degraded experience for anyone using assistive technology, there's real legal exposure. Depending on your market, you may be subject to the ADA (US), AODA (Ontario), EN 301 549 (EU), or similar legislation, most of which anchor to &lt;strong&gt;WCAG&lt;/strong&gt; as the technical standard. Manual audits catch problems after they ship; automation catches them before they merge.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No single tool covers everything, so we layered four:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;When it runs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Static / lint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;eslint-plugin-jsx-a11y&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Every save &amp;amp; CI lint job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unit / integration&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;vitest&lt;/code&gt; + &lt;code&gt;axe-core&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Every test run &amp;amp; CI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visual / interaction&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@storybook/addon-a11y&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Storybook CI build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI gate&lt;/td&gt;
&lt;td&gt;Dedicated workflow&lt;/td&gt;
&lt;td&gt;Every PR, blocks merge&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each layer catches a class of defect the previous one can't. Let's go through them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: Static analysis with &lt;code&gt;eslint-plugin-jsx-a11y&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The cheapest place to catch a defect is in the editor, before the code is even saved. &lt;code&gt;eslint-plugin-jsx-a11y&lt;/code&gt; statically analyzes JSX and flags a surprising number of issues: missing &lt;code&gt;alt&lt;/code&gt; text, invalid ARIA attributes, click handlers on non-interactive elements, and so on.&lt;/p&gt;

&lt;p&gt;Use the &lt;strong&gt;strict&lt;/strong&gt; preset. The recommended preset downgrades several rules to warnings, and warnings are noise that everyone learns to ignore.&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 (the idea, not the full config)&lt;/span&gt;
&lt;span class="nx"&gt;rules&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;jsxA11y&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flatConfigs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;strict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// escalate anything strict still leaves as a warning&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jsx-a11y/no-autofocus&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;error&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;What it catches:&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="c1"&gt;// ❌ Lint error: click handler on a non-interactive element&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;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;handleSelect&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Select plan&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Interactive semantics come for free with the right element&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;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"button"&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;handleSelect&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Select plan&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it &lt;em&gt;can't&lt;/em&gt; catch: anything that only exists at runtime: computed ARIA states, focus order, contrast, or how components compose together. For that, we need the DOM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: Unit tests with axe-core
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/dequelabs/axe-core" rel="noopener noreferrer"&gt;axe-core&lt;/a&gt; is the de facto standard accessibility rules engine. Running it against rendered components in your unit test suite means every component gets scanned on every test run, with zero extra CI plumbing.&lt;/p&gt;

&lt;p&gt;The key to making this sustainable is a shared helper so every test looks identical. Ours renders the component, runs axe, and exposes both the scan results and the usual testing-library queries:&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="c1"&gt;// test-utils/renderA11y.tsx (sketch)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;renderA11y&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReactElement&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ui&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;axeResults&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;axe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;runOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tag&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;values&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="s1"&gt;wcag2a&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;wcag2aa&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;wcag21aa&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;wcag22aa&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="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;axeResults&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;Pair it with an assertion helper (an &lt;code&gt;expectNoViolations(axeResults)&lt;/code&gt;) that formats violations into readable strings before asserting. That detail matters more than it looks: when a test fails, the developer should see &lt;em&gt;which rule&lt;/em&gt;, &lt;em&gt;which node&lt;/em&gt;, and &lt;em&gt;what HTML&lt;/em&gt;, not an opaque object diff.&lt;/p&gt;

&lt;p&gt;Every component then gets a co-located &lt;code&gt;*.a11y.test.tsx&lt;/code&gt; file:&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="c1"&gt;// Switch.a11y.test.tsx (the shape of every such file)&lt;/span&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="s1"&gt;has no axe violations&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;axeResults&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;renderA11y&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Switch&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Enable notifications"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expectNoViolations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;axeResults&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// ...plus targeted checks: aria state exposed to AT, keyboard operability, focus&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Beyond the axe scan, add a few targeted assertions per component: the ARIA state is exposed (&lt;code&gt;aria-checked&lt;/code&gt; on a switch), it's reachable by &lt;code&gt;Tab&lt;/code&gt;, and it's operable by keyboard (Space/Enter toggles it).&lt;/p&gt;

&lt;p&gt;Two practical notes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test meaningful states, not just the default render.&lt;/strong&gt; An accordion that passes axe while collapsed can still fail when expanded. Scan open modals, expanded menus, error states, and disabled states.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enforce the convention structurally.&lt;/strong&gt; A rule that lives only in a wiki dies in a wiki. We enforce the co-located test file two ways: it's a line item on the PR contribution checklist, and our component scaffolding (a &lt;a href="https://plopjs.com/" rel="noopener noreferrer"&gt;plop&lt;/a&gt; generator) creates the &lt;code&gt;*.a11y.test.tsx&lt;/code&gt; file automatically for every new component. The path of least resistance &lt;em&gt;is&lt;/em&gt; the compliant path.&lt;/p&gt;

&lt;h3&gt;
  
  
  The jsdom gotcha: no contrast checking
&lt;/h3&gt;

&lt;p&gt;Here's the trap that catches almost everyone: &lt;strong&gt;axe-core running in jsdom cannot detect color-contrast violations.&lt;/strong&gt; jsdom doesn't implement a CSS layout engine or Canvas, so axe has no way to compute foreground/background colors. The &lt;code&gt;color-contrast&lt;/code&gt; rule silently reports "incomplete" rather than failing.&lt;/p&gt;

&lt;p&gt;If your only accessibility testing is axe-in-jsdom, you have a false sense of security about the single most common WCAG failure on the web. Contrast requires a real browser, which brings us to layer 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: Storybook as a browser-based scanner
&lt;/h2&gt;

&lt;p&gt;If you maintain a design system, you almost certainly already have Storybook, and every component already has stories covering its visual states. &lt;code&gt;@storybook/addon-a11y&lt;/code&gt; runs axe against each story &lt;em&gt;in a real browser&lt;/em&gt;, which means contrast checks actually work.&lt;/p&gt;

&lt;p&gt;Wire it globally in &lt;code&gt;preview.tsx&lt;/code&gt; so every story is scanned by default. Three decisions matter here, not the boilerplate:&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;// preview.tsx -&amp;gt; parameters.a11y (the decisions that matter)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;runOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tag&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="cm"&gt;/* your WCAG target tags */&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;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// fail the story in test runs, don't just warn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is: pin the run config to your WCAG target so Storybook and unit tests check the same standard, set it &lt;em&gt;once&lt;/em&gt; globally rather than per story, and use &lt;code&gt;'error'&lt;/code&gt; so violations fail rather than warn.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making suppressions traceable
&lt;/h3&gt;

&lt;p&gt;Sometimes a story legitimately needs to skip the scan: a deliberately broken example in documentation, or a known issue with a fix scheduled. The failure mode to avoid is the silent, permanent suppression that nobody remembers adding.&lt;/p&gt;

&lt;p&gt;Our rule: &lt;strong&gt;you can suppress a scan, but only with a ticket attached.&lt;/strong&gt; The helper throws at runtime if the ticket reference doesn't match your tracker's key format:&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;// a11yException (sketch): a suppression is only valid with a ticket attached&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;a11yException&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reason&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;ticket&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;reason&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;TICKET_PATTERN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;suppression must reference a tracked issue&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="na"&gt;a11y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;disable&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;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`disabled: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ticket&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;span class="c1"&gt;// usage in a story&lt;/span&gt;
&lt;span class="nl"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;a11yException&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DS-421&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fails contrast pending token audit&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 turns suppressions from invisible debt into a queryable backlog: grep for &lt;code&gt;a11yException&lt;/code&gt; and you have your remediation list, each entry pointing at a live ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 4: The CI gate, and why we didn't turn it on immediately
&lt;/h2&gt;

&lt;p&gt;The final layer is a dedicated CI workflow that blocks merge on any violation:&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="c1"&gt;# a11y.yml (the essential shape)&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pull_request&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;storybook-a11y&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# checkout, node, install...&lt;/span&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;pnpm --filter storybook build&lt;/span&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;pnpm --filter storybook test-storybook --ci&lt;/span&gt;  &lt;span class="c1"&gt;# axe against every story&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the division of labor: the axe &lt;strong&gt;unit tests&lt;/strong&gt; already run in the main CI pipeline alongside everything else (&lt;code&gt;turbo test&lt;/code&gt; picks them up like any other test file, no special workflow needed). The dedicated workflow exists specifically for the &lt;strong&gt;browser-based&lt;/strong&gt; Storybook scan, because that's the only place contrast and interaction rules can run.&lt;/p&gt;

&lt;h3&gt;
  
  
  The rollout trap: don't gate before you audit
&lt;/h3&gt;

&lt;p&gt;Here's the strategic decision that saved us weeks of pain. Flipping the CI gate on is trivial; the question is &lt;em&gt;when&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If your color tokens haven't been audited against your contrast target, activating a browser-based contrast gate means &lt;strong&gt;every PR that touches a colored component fails&lt;/strong&gt;, through no fault of the PR author. Nothing kills a quality initiative faster than a gate the team perceives as arbitrary and unfixable at the PR level.&lt;/p&gt;

&lt;p&gt;So we phased it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Phase 1.&lt;/strong&gt; Ship the lint rules and axe unit tests. These fail only on defects a developer can actually fix in their PR. Zero contrast rules, zero unfair failures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 1b.&lt;/strong&gt; Ship the Storybook addon in &lt;em&gt;report&lt;/em&gt; mode. Violations are visible in the Storybook UI and in CI logs, building awareness without blocking anyone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token audit.&lt;/strong&gt; Run every color token pair through a contrast checker against your target ratio. Fix the tokens &lt;em&gt;at the token level&lt;/em&gt;, so one fix propagates everywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 2.&lt;/strong&gt; Only after the audit is green, flip the workflow to blocking. Now every failure is a genuine regression, and the gate has credibility.
The sequencing principle generalizes: &lt;strong&gt;automated gates should only fail on things the person seeing the failure can fix.&lt;/strong&gt; Systemic debt gets fixed systemically, then the gate protects the clean state.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Choosing a target: AA required, AAA where it's cheap
&lt;/h2&gt;

&lt;p&gt;WCAG 2.2 &lt;strong&gt;AA&lt;/strong&gt; is the right baseline: it's what most legislation references, and it's achievable for every component. We additionally target &lt;strong&gt;AAA color contrast (7:1)&lt;/strong&gt; and enhanced focus indicators where the design tokens allow, for a simple reason: contrast is decided once, centrally, in the token palette. If your foundation tokens can clear 7:1, every consumer gets AAA contrast for free, and you've built headroom against future tightening of standards. Where AAA would force genuinely worse design trade-offs, AA stands.&lt;/p&gt;

&lt;h2&gt;
  
  
  A side benefit: accessibility tests audit your types
&lt;/h2&gt;

&lt;p&gt;An unexpected payoff from writing an axe test for every existing component: the process surfaced several &lt;strong&gt;prop-type gaps&lt;/strong&gt; that had been silently reducing type safety: a component whose props type omitted &lt;code&gt;children&lt;/code&gt; even though it rendered them, another missing its &lt;code&gt;onChange&lt;/code&gt; in the public type, a generic defaulting to an empty record. Writing tests that exercise components the way real consumers do is a forcing function that shakes these out. Log them as they surface; each one is coverage fidelity you're currently losing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;No single tool gives you accessibility coverage. The layers compose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lint&lt;/strong&gt; catches structural mistakes at author time, for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;axe unit tests&lt;/strong&gt; catch runtime ARIA and semantics defects on every test run, but &lt;em&gt;cannot&lt;/em&gt; check contrast in jsdom.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storybook + addon-a11y&lt;/strong&gt; runs axe in a real browser, closing the contrast gap using stories you already have.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A blocking CI gate&lt;/strong&gt; makes the standard non-optional, but only flip it on after you've audited the systemic debt it would surface.
And thread traceability through all of it: every suppression carries a ticket, every new component scaffolds its own test, every convention is enforced by tooling rather than memory. The design system is where accessibility leverage lives: instrument it once, and every product you ship inherits the guarantee.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I still think about that conversation. As a dev, you can actually make someone's life better. Don't forget this. And so you &lt;em&gt;can't&lt;/em&gt; forget it, encode it into the pipeline: then the reminder arrives on every save, every test run, every pull request.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>architecture</category>
      <category>frontend</category>
      <category>testing</category>
    </item>
    <item>
      <title>CRM data, Service Adapter and Integration Contract</title>
      <dc:creator>DimiDan</dc:creator>
      <pubDate>Mon, 03 Aug 2026 19:46:38 +0000</pubDate>
      <link>https://dev.to/dimidan/crm-integration-shall-we-talk-about-it-3kki</link>
      <guid>https://dev.to/dimidan/crm-integration-shall-we-talk-about-it-3kki</guid>
      <description>&lt;p&gt;You've got one CRM integration in production and it's been fine for months. Now there's a second one to wire up. Same contract, same schema, same node shape. Should be a couple of days.&lt;/p&gt;

&lt;p&gt;You pull the first payload from CRM B and put it next to CRM A out of habit.&lt;/p&gt;

&lt;p&gt;CRM A, a monetary field:&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;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"estimated_cost"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1275.43&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"spec"&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;"number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"decimal"&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;CRM B, same kind of field:&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;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6624"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"spec"&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;"number"&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;Huh.&lt;/p&gt;

&lt;p&gt;One's a JSON number. One's a string. One tags the format, one doesn't. Both say &lt;code&gt;"type": "number"&lt;/code&gt;. And every validator between those adapters and your UI is perfectly happy with both.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what's the issue
&lt;/h2&gt;

&lt;p&gt;This is the part that takes a minute to sit with. You go looking for who got it wrong, and nobody did.&lt;/p&gt;

&lt;p&gt;Go read the contract. It says there's a field called &lt;code&gt;value&lt;/code&gt;. That's it. It never said what could go &lt;em&gt;in&lt;/em&gt; &lt;code&gt;value&lt;/code&gt;. So the person building CRM A looked at a number and sent a number. The person building CRM B looked at the same thing and sent a string, probably because that CRM's own API hands back strings. Both reasonable. Both shipped. Both correct against the spec as written.&lt;/p&gt;

&lt;p&gt;The contract wasn't violated. It just didn't have an opinion, and two people filled the silence differently.&lt;/p&gt;

&lt;p&gt;That's the whole category of problem I want to talk about, because it doesn't show up in code review and it doesn't show up in CI. It shows up eight months later when someone asks why one CRM's totals are off by a cent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The good idea underneath
&lt;/h2&gt;

&lt;p&gt;Before piling on, credit where it's due, because the foundation here is right.&lt;/p&gt;

&lt;p&gt;Every source produces the same node. Consumers never branch on where the data came from.&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;FieldNode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&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="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;value&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="nx"&gt;spec&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="nx"&gt;treePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FieldNode&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;One shape, one tree, one set of consumers. Adding a CRM is adapter work and nothing above the boundary moves. This genuinely scales, and if you're designing this today, start here.&lt;/p&gt;

&lt;p&gt;But notice what it does to your risk. Two fields carry all the meaning — &lt;code&gt;value&lt;/code&gt; and &lt;code&gt;spec&lt;/code&gt;. The rest is plumbing. Leave those two vague and the uniformity is a paint job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 1: name the field, then actually say what goes in it
&lt;/h2&gt;

&lt;p&gt;Here's the type that caused everything above:&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;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="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks like a decision. Isn't one. It's a union that permits both answers, which means you'll get both answers the moment two adapters get written in parallel by people who never talk.&lt;/p&gt;

&lt;p&gt;So say the thing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;value&lt;/code&gt; is a decimal string, or null. Never a JSON number.&lt;/li&gt;
&lt;li&gt;Containers (&lt;code&gt;array&lt;/code&gt;, &lt;code&gt;object&lt;/code&gt;) carry &lt;code&gt;null&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Numeric nodes carry a &lt;code&gt;format&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;format: 'money'&lt;/code&gt; also carries &lt;code&gt;currency&lt;/code&gt;, ISO 4217.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one isn't bureaucracy. &lt;code&gt;format: 'decimal'&lt;/code&gt; can't tell a cost from a tax rate. They're both decimals. Only the source system knows which one is money, and that fact turns out to constrain your whole architecture. Hold that thought for Rule 3.&lt;/p&gt;

&lt;p&gt;And if you're about to argue you don't need strings because you're not doing currency math: &lt;code&gt;value: number&lt;/code&gt; still eats integers past 2^53, which is where record IDs live. It mangles anything past 15 significant digits. It'll hand you &lt;code&gt;1e+21&lt;/code&gt; when you least want it. A decimal string has none of those problems and costs you nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 2: shut the door on undeclared fields
&lt;/h2&gt;

&lt;p&gt;Here's the sequel to the story, and it's more common than the first part.&lt;/p&gt;

&lt;p&gt;CRM A's adapter stamps &lt;code&gt;format: "decimal"&lt;/code&gt; on every numeric field. Handy. Consumers start reading it. Someone builds a formatting rule on top of it.&lt;/p&gt;

&lt;p&gt;Except &lt;code&gt;format&lt;/code&gt; was never declared in the schema. It's just riding along in the payload — invisible to validation, missing from your generated client types, guaranteed by nobody. And CRM B doesn't send it at all, so the formatting rule works for half your customers.&lt;/p&gt;

&lt;p&gt;Whatever flavor you get, it's one bug: &lt;strong&gt;data present, schema silent.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One flag closes the whole category. &lt;code&gt;additionalProperties: false&lt;/code&gt; in OpenAPI, &lt;code&gt;.strict()&lt;/code&gt; in zod, whatever yours is called. Undeclared fields stop being possible. Everything else you do about this is whack-a-mole.&lt;/p&gt;

&lt;p&gt;Fair warning: turning it on will break things that currently work, because at least one of those undeclared fields is load-bearing somewhere. That's not a reason to skip it. The dependency was already there — you just couldn't see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 3: the normalization layer can't save you
&lt;/h2&gt;

&lt;p&gt;Okay, so the obvious fix. Build a normalization layer. One place that takes whatever each adapter produces and forces it into canonical form. Money becomes a decimal string, numerics get a format tag, everyone goes home.&lt;/p&gt;

&lt;p&gt;You do need that layer. It also can't do the job on its own, and this is the bit I'd most want to hand to past-me.&lt;/p&gt;

&lt;p&gt;Picture a money field whose true value is &lt;code&gt;450.20&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adapter one does &lt;code&gt;JSON.parse&lt;/code&gt; on the CRM response. The value lands in a float64, comes back out as &lt;code&gt;"450.2"&lt;/code&gt;. Normalization checks it: it's a string, it matches a decimal literal, format's present. &lt;strong&gt;Passes.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Adapter two reads the response as text and never touches a float. Emits &lt;code&gt;"450.20"&lt;/code&gt;. Normalization checks it. &lt;strong&gt;Passes, identically.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your layer cannot tell these apart. Both are well-formed strings. One of them quietly threw information away, and no amount of schema work will catch it, because a validator can only confirm the shape of what you handed it. It can't reconstruct what got dropped before it ever saw the data.&lt;/p&gt;

&lt;p&gt;So this isn't one boundary. It's two, doing different jobs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Boundary&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Why it can't fold into the other&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Read-time&lt;/strong&gt; — inside each adapter&lt;/td&gt;
&lt;td&gt;Decode source numerics as text. &lt;code&gt;json.Number&lt;/code&gt; in Go, a reviver in JS, never bare &lt;code&gt;JSON.parse&lt;/code&gt;. Tag &lt;code&gt;format&lt;/code&gt; and &lt;code&gt;currency&lt;/code&gt; from provider field metadata — HubSpot's Properties API, Salesforce &lt;code&gt;describe()&lt;/code&gt;.&lt;/td&gt;
&lt;td&gt;It's the only layer that touches the raw HTTP response. Precision lives or dies here and nowhere else. It's also the only place that knows &lt;code&gt;Amount&lt;/code&gt; is money and &lt;code&gt;source_user_id&lt;/code&gt; isn't.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Enforcement&lt;/strong&gt; — the shared service&lt;/td&gt;
&lt;td&gt;Strict schema. Validate your own output. Fail closed. Derive &lt;code&gt;format&lt;/code&gt; when an adapter didn't send one. Own the spec.&lt;/td&gt;
&lt;td&gt;It's the only layer that sees every adapter side by side. One stamping &lt;code&gt;format&lt;/code&gt; while another skips it is completely invisible from inside either adapter.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read-time on its own is just a promise everyone makes and nobody keeps, because "be careful with floats" isn't a mechanism. Enforcement on its own validates already-damaged data and hands you a green checkmark.&lt;/p&gt;

&lt;p&gt;If you keep one line from this post: &lt;strong&gt;the guarantee gets created upstream of where it gets checked.&lt;/strong&gt; Any design that stuffs all the normalization into one shared service has a hole in it that more schema won't fill.&lt;/p&gt;

&lt;p&gt;This is also, incidentally, why "which service owns this fix?" can sit open in a doc for weeks. Every answer is partly right and none of them is enough. When that happens, the question is usually the problem. This one quietly assumed there was one owner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 4: the mapper that quietly eats your fields
&lt;/h2&gt;

&lt;p&gt;Different failure, same silence.&lt;/p&gt;

&lt;p&gt;Early on, your wire type and your domain type are the same type. The only mapper that compiles is a spread:&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;fields&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;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display_label&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing can go missing. Not because anyone's being careful — because there's no other way to write it.&lt;/p&gt;

&lt;p&gt;Then the domain type picks up something the wire doesn't have. A render hint, say, or a UI kind. Perfectly reasonable in isolation. But now the two types have forked, so you need a translation function, and a translation function is a list of fields:&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;const&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ApiField&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;FieldNode&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;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&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="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display_label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;group&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;leaf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&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="na"&gt;treePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&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;map&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="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whatever isn't on that list is gone. Including every field anyone adds to the contract next quarter. And you won't catch it in review, because that mapper is &lt;em&gt;correct&lt;/em&gt; for the types it was handed.&lt;/p&gt;

&lt;p&gt;Derive the domain type from the wire type instead, and forgetting a field becomes a compile error:&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;FieldNode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Omit&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;WireField&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;display_label&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&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;Worth saying the other half too: the spread version preserved fields by being transparent, not by being right. It happily carried undeclared junk along with everything else, which is Rule 2's problem wearing a different hat. You want both halves. &lt;strong&gt;Parse strictly, then pass through.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 5: don't add a second way to say what something is
&lt;/h2&gt;

&lt;p&gt;Tempting move: put a declared "kind" field next to your structural type. &lt;code&gt;spec.type&lt;/code&gt; plus tree position already tells you everything behavioral — expandable, container, nested — but a &lt;code&gt;kind&lt;/code&gt; field feels more explicit, so it goes in.&lt;/p&gt;

&lt;p&gt;Two things go wrong. It drifts from the structure it's supposed to describe, which is the boring failure. And it tends to land on a property name the wire already uses for something else. Look at &lt;code&gt;type&lt;/code&gt; in that mapper above: it's the UI kind in the domain type and the source record type on the wire. That collision is exactly what forced the field-enumerating translation in Rule 4.&lt;/p&gt;

&lt;p&gt;Cheap thing to try on your own code right now: for every enum on a core domain type, grep for reads of each member. The ones that get written and never read are telling you the type is carrying a distinction nothing actually needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The schema - what it should enforce
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;value&lt;/code&gt; never gets coerced.&lt;/strong&gt; &lt;code&gt;z.coerce.string()&lt;/code&gt; would quietly accept a raw number and enforce absolutely nothing. You want a bad value to &lt;em&gt;fail&lt;/em&gt;, not get tidied up behind your back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The decimal-literal regex is your precision tripwire.&lt;/strong&gt; Anything that went through a float and back emits exponent notation at large magnitudes, and &lt;code&gt;1e+21&lt;/code&gt; doesn't match. It only catches the loud cases. The quiet ones are Rule 3's job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;spec&lt;/code&gt; is required.&lt;/strong&gt; Optional-chaining a discriminator is itself the bug. &lt;code&gt;spec?.type&lt;/code&gt; makes "field missing" and "value unknown" look identical, and both slide into the same default branch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;currency&lt;/code&gt; sits next to &lt;code&gt;format&lt;/code&gt;, not inside it.&lt;/strong&gt; Rule 1's point: only the source system knows which decimals are money, so that tag has to be born at read time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to actually enforce this
&lt;/h2&gt;

&lt;p&gt;Writing the schema is the easy part. Where you run it decides whether it does anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The producer validates its own output.&lt;/strong&gt; Your shared service checks its response before sending and fails closed. Skip this and consumer-side validation just turns a silent bug into a loud one, later, in somebody else's service, at 2am.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The consumer parses at the boundary, per node.&lt;/strong&gt; A recursive strict schema blows up the entire tree over one malformed grandchild, which means a whole panel disappears because of one bad leaf. Walk the tree, validate node by node, keep the bad ones and mark them unavailable, and report them. Silently dropping them is its own kind of lie — the user just sees a shorter list and no explanation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And know what none of this covers.&lt;/strong&gt; A perfectly-shaped value that lost precision upstream passes every check on that list. The read-time boundary needs a shared decode helper and human review, not a test. Fixture-based tests have a cousin of the same blind spot: they'll stay green while the same code fails on the live route, because your fixtures carry fields the real projection drops. Make at least one test exercise the projection itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;For every field in your contract: is the &lt;em&gt;content&lt;/em&gt; specified, or just the name? A union that permits two encodings is an unspecified field wearing a type annotation.&lt;/li&gt;
&lt;li&gt;Is &lt;code&gt;additionalProperties: false&lt;/code&gt; on? If not, you have undeclared fields in production right now, and one of them matters.&lt;/li&gt;
&lt;li&gt;Where does precision get created versus checked? Different layers means your validator can't verify the thing it looks like it's verifying.&lt;/li&gt;
&lt;li&gt;Does any mapper enumerate fields? It's dropping something today and it'll drop everything you add tomorrow.&lt;/li&gt;
&lt;li&gt;Does the producer validate its own output, or only the consumer?&lt;/li&gt;
&lt;li&gt;Which members of your core enums actually get read?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A contract isn't the fields you named. It's the behaviors you made impossible.&lt;/p&gt;

</description>
      <category>crm</category>
      <category>architecture</category>
      <category>data</category>
    </item>
    <item>
      <title>Feels like a feature until it's infrastructure</title>
      <dc:creator>DimiDan</dc:creator>
      <pubDate>Mon, 27 Jul 2026 16:01:18 +0000</pubDate>
      <link>https://dev.to/dimidan/feels-like-a-feature-until-its-infrastructure-56mo</link>
      <guid>https://dev.to/dimidan/feels-like-a-feature-until-its-infrastructure-56mo</guid>
      <description>&lt;p&gt;A while ago I wrote about &lt;a href="https://dev.to/dimidan/pricing-logic-feels-boring-until-its-wrong-4m4e"&gt;pricing logic&lt;/a&gt; and why it deserves to be treated as infrastructure instead of a pile of UI code. This is the same lesson from a different subsystem: undo/redo in a real-time, event-driven editor. The difference is that this time the payoff is visible, and it showed up a year later in a place we weren't looking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Features and invariants are different objects
&lt;/h2&gt;

&lt;p&gt;Features have a definition of done you can see. Ship the button, click the button, close the ticket. When a feature half-works, someone notices, because the feature is the thing people look at.&lt;/p&gt;

&lt;p&gt;Infrastructure is different. Its job is to hold invariants that other code relies on. For undo/redo the invariant is brutal: every state mutation, from every code path, present and future, must be reversible in chronological order. That invariant doesn't live in any one component. You can't screenshot it. When it erodes, it erodes silently, one unmigrated code path at a time.&lt;/p&gt;

&lt;p&gt;Pricing had the same shape. The invariant there was "money math is exact and happens in exactly one place." Every shortcut that looked harmless locally — a &lt;code&gt;parseFloat&lt;/code&gt; here, a float multiplication there — chipped at an invariant nobody owned. It was boring until it was wrong.&lt;/p&gt;

&lt;p&gt;The thing that decides whether you get this right is a question asked early: is this a feature, or is it infrastructure wearing a feature costume?&lt;/p&gt;

&lt;h2&gt;
  
  
  The tell: closures vs commands
&lt;/h2&gt;

&lt;p&gt;You can often tell which mental model produced a piece of code by looking at what the abstraction knows about itself.&lt;/p&gt;

&lt;p&gt;The obvious way to build undo support is callbacks. Every widget that wants to participate registers a pair of closures: here's how to undo me, here's how to redo me. It works, and it's the version most editors start with.&lt;/p&gt;

&lt;p&gt;The problem is that a callback-based undo entry knows nothing. It's two anonymous closures. You can't ask it what it does, which block it touches, whether it succeeded, or when it happened. You can only fire it and hope. Debugging means reading captured variables in a debugger. Testing means reconstructing the exact closure environment.&lt;/p&gt;

&lt;p&gt;The command version is self-describing:&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;DeleteBlockCommand&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;timestamp&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="nf"&gt;getCurrentBlockId&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="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;undo&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;redo&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That difference looks cosmetic. It is not. Because commands carry identity, the system built on them can do things the closure version never could:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One chronological stack across text edits, block moves, and table changes, because everything is comparable.&lt;/li&gt;
&lt;li&gt;Retry and failure semantics, because &lt;code&gt;undo()&lt;/code&gt; returns a result instead of throwing into the void.&lt;/li&gt;
&lt;li&gt;Batching, because commands compose.&lt;/li&gt;
&lt;li&gt;Analytics at a handful of seams instead of everywhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When architecture pays off
&lt;/h2&gt;

&lt;p&gt;That last bullet deserves its own section, because it's where the decision paid for itself.&lt;/p&gt;

&lt;p&gt;When we wrote the RFC for the command architecture, one of its stated purposes was to reify editor actions as discrete, self-describing objects. Text edit, block move, style change, table mutation — each one individually addressable, with its own scope, timestamp, payload, and success or failure resolution, all landing on one chronological stack. Granular, per-event control over what happens in the editor. The justification at the time was correctness: reliable undo, reliable ordering, reliable failure handling.&lt;/p&gt;

&lt;p&gt;Then product came asking for interaction tracking. Which actions do users perform, how often, in what order.&lt;/p&gt;

&lt;p&gt;Look at what an analytics event needs: a name, a subject, a timestamp, structured properties, and confirmation that the thing actually happened. That is a command object. The grain analytics needed was the grain the architecture already had.&lt;/p&gt;

&lt;p&gt;Without the command layer, per-action tracking means scattering emit calls across dozens of UI components and hoping every future component remembers to do the same. It also means every one of those call sites independently deciding what counts as success, which in practice means some of them fire on intent rather than outcome and your funnel quietly overcounts.&lt;/p&gt;

&lt;p&gt;With the command layer, tracking attaches at a handful of well-defined seams and inherits success-gating for free. No event fires for an action that failed or got rejected, because the command already had to know the difference in order to be undoable.&lt;/p&gt;

&lt;p&gt;We didn't design the command pattern for Mixpanel. We designed it for control over events, and analytics turned out to be just another consumer of that control. That's the signature of an infrastructure decision: consumers it wasn't designed for show up later and find what they need already there.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to spot infrastructure wearing a feature costume
&lt;/h2&gt;

&lt;p&gt;Four questions that would have flagged both pricing and undo/redo early:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Does it cut across features?&lt;/strong&gt; If every new widget has to remember to participate, it's infrastructure, and "remember to" is not a mechanism.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does it fail silently?&lt;/strong&gt; Features fail loudly, in someone's face. Invariants fail quietly, in aggregate, and the first report is a metric that looks slightly off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is correctness invisible?&lt;/strong&gt; If you can't tell it's broken by looking at the screen, you need the architecture to make wrongness impossible, not code review to catch it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is "mostly done" meaningfully different from done?&lt;/strong&gt; For a feature, eighty percent shipped is roughly eighty percent of the value. For an invariant it's zero: a single unmigrated code path is enough to make the guarantee untrue, and untrue guarantees are worse than absent ones because everything downstream is built on them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The uncomfortable part is that infrastructure decisions are hard to justify during planning, because the strongest argument for one is a consumer you can't name yet. You're asking for extra work now against a payoff you can only describe in the abstract.&lt;/p&gt;

&lt;p&gt;A year later, ours had a name.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>javascript</category>
      <category>react</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Claude Code, Bun and TypeScript</title>
      <dc:creator>DimiDan</dc:creator>
      <pubDate>Mon, 27 Jul 2026 12:50:21 +0000</pubDate>
      <link>https://dev.to/dimidan/claude-code-bun-and-typescript-cjj</link>
      <guid>https://dev.to/dimidan/claude-code-bun-and-typescript-cjj</guid>
      <description>&lt;h1&gt;
  
  
  Why Claude Code runs on Bun: runtime tradeoffs in TypeScript CLI tooling
&lt;/h1&gt;

&lt;p&gt;Anthropic recently shipped Claude Code — their agentic CLI coding assistant — on Bun instead of Node.js. For most product announcements, the runtime choice would be a footnote. Here it's worth unpacking, because the tradeoffs Anthropic navigated are exactly the ones you hit when building or evaluating TypeScript-heavy developer tooling: startup latency, bundling strategy, native module compatibility, and what "good enough" dependency management actually looks like in 2025.&lt;/p&gt;

&lt;p&gt;This isn't a Bun vs. Node benchmarking post. It's an examination of &lt;em&gt;why&lt;/em&gt; the decision makes sense for a CLI tool specifically, what it signals about the broader ecosystem, and where the tradeoffs still bite you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why runtime choice matters more for CLIs than for servers
&lt;/h2&gt;

&lt;p&gt;For a long-running server process, Node.js startup cost of 50–150ms is irrelevant — you pay it once. For a CLI invoked dozens of times per development session, cold-start latency is a first-class UX concern.&lt;/p&gt;

&lt;p&gt;Bun's startup time is consistently in the 5–15ms range for a simple script. Node.js lands closer to 50–80ms before your first line of application code runs. That delta is imperceptible in a single invocation. Run a CLI 30 times in a session and you've saved a couple of seconds — more importantly, you've removed the subjective sense of lag that makes a tool feel heavy.&lt;/p&gt;

&lt;p&gt;This is the same reason Deno has gained traction in scripting contexts despite losing the server-side battle to Node. Fast startup is a feature, and for AI-assisted tooling where the human is waiting in a tight feedback loop, it matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bun's bundler as a distribution primitive
&lt;/h2&gt;

&lt;p&gt;Bun ships a first-party bundler. For a CLI, this is significant. The standard Node.js distribution story for a TypeScript CLI involves:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Compile TypeScript with &lt;code&gt;tsc&lt;/code&gt; or &lt;code&gt;esbuild&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Bundle with &lt;code&gt;esbuild&lt;/code&gt; or &lt;code&gt;rollup&lt;/code&gt; to collapse the dependency graph&lt;/li&gt;
&lt;li&gt;Either ship &lt;code&gt;node_modules&lt;/code&gt; (large, fragile) or use a tool like &lt;code&gt;pkg&lt;/code&gt; or &lt;code&gt;nexe&lt;/code&gt; to produce a single executable&lt;/li&gt;
&lt;li&gt;Handle platform-specific native binaries separately&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Bun collapses steps 1–3 into a single command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun build ./src/index.ts &lt;span class="nt"&gt;--compile&lt;/span&gt; &lt;span class="nt"&gt;--outfile&lt;/span&gt; claude-code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--compile&lt;/code&gt; flag produces a self-contained binary that embeds the Bun runtime. No Node.js installation required on the target machine. For a CLI distributed via npm (&lt;code&gt;npm install -g @anthropic-ai/claude-code&lt;/code&gt;), this matters less — you can assume Node is present. But for future distribution channels (Homebrew, direct download, CI runner images), single-binary output is a meaningful operational simplification.&lt;/p&gt;

&lt;p&gt;Bun's bundler also strips unused exports aggressively. A TypeScript codebase pulling in large SDKs — the Anthropic SDK, tree-sitter bindings, various language servers — can produce a substantially smaller artifact than a naive &lt;code&gt;tsc&lt;/code&gt; + ship-everything approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dependency management story is genuinely better
&lt;/h2&gt;

&lt;p&gt;Bun's package manager is faster than npm by a wide margin — typically 10–25× on a cold install, faster still on cache hits. For a tool like Claude Code that's installed globally and occasionally updated, this shows up as a noticeably snappier install.&lt;/p&gt;

&lt;p&gt;Bun uses a binary lockfile (&lt;code&gt;bun.lockb&lt;/code&gt;) that's more compact and faster to parse than &lt;code&gt;package-lock.json&lt;/code&gt;, but it's not human-readable. If you care about auditing lockfile diffs in code review, you'll need to run &lt;code&gt;bun install --frozen-lockfile&lt;/code&gt; in CI and accept that the lockfile diff in your PR is opaque.&lt;/p&gt;

&lt;p&gt;More substantively: Bun is &lt;code&gt;node_modules&lt;/code&gt;-compatible. It doesn't invent a new module resolution scheme — your existing &lt;code&gt;package.json&lt;/code&gt; works. Native modules compiled against Node's ABI &lt;em&gt;mostly&lt;/em&gt; work, with exceptions for modules using non-public V8 internals. That's where the real compatibility risk sits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where native modules still cause pain
&lt;/h2&gt;

&lt;p&gt;Bun uses JavaScriptCore (JSC) instead of V8. For pure JavaScript and TypeScript, this is transparent. For native addons compiled as &lt;code&gt;.node&lt;/code&gt; files via &lt;code&gt;node-gyp&lt;/code&gt;, the situation is more complicated.&lt;/p&gt;

&lt;p&gt;Modules that use the Node-API (N-API) surface — the stable ABI layer introduced specifically for this kind of portability — generally work fine under Bun. Modules that reach into V8 internals or use older &lt;code&gt;nan&lt;/code&gt;-based bindings may not.&lt;/p&gt;

&lt;p&gt;For a coding assistant CLI, the relevant native dependencies are typically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tree-sitter language parsers (N-API based, generally fine)&lt;/li&gt;
&lt;li&gt;OS keychain access (platform-specific, usually N-API)&lt;/li&gt;
&lt;li&gt;File system watchers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anthropic presumably validated these before shipping. But if you're evaluating Bun for your own TypeScript tooling and you depend on something like &lt;code&gt;sharp&lt;/code&gt;, &lt;code&gt;better-sqlite3&lt;/code&gt;, or &lt;code&gt;canvas&lt;/code&gt;, verify native compatibility explicitly before committing.&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;// Quick compatibility check — run under both Bun and Node, compare output&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createRequire&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="s1"&gt;module&lt;/span&gt;&lt;span class="dl"&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;require&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createRequire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&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="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;nativeAddon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./build/Release/addon.node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Native addon loaded:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nativeAddon&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="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Native addon failed:&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="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;message&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;h2&gt;
  
  
  TypeScript ergonomics: where Bun earns its keep
&lt;/h2&gt;

&lt;p&gt;Bun executes TypeScript directly without a compilation step — no &lt;code&gt;ts-node&lt;/code&gt;, no &lt;code&gt;tsx&lt;/code&gt;, no &lt;code&gt;esbuild-register&lt;/code&gt;. You write a &lt;code&gt;.ts&lt;/code&gt; file and run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun run src/index.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not TypeScript type-checking. Bun strips types and runs the resulting JavaScript. But for iterating on tooling internals, removing the compilation step from the inner loop is a meaningful quality-of-life improvement.&lt;/p&gt;

&lt;p&gt;For production builds of a distributed CLI, you still want &lt;code&gt;tsc --noEmit&lt;/code&gt; as a type-check step in CI. The Bun model is: use JSC for fast execution, use TypeScript's type checker separately for correctness. These are separable concerns, and Bun's approach of not conflating them is arguably more honest than &lt;code&gt;ts-node&lt;/code&gt;'s.&lt;/p&gt;

&lt;p&gt;A CI configuration that reflects this split:&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="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Type check&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 tsc --noEmit&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test&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;bun test&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build binary&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;bun build ./src/index.ts --compile --outfile dist/claude-code&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What this signals for the TypeScript tooling ecosystem
&lt;/h2&gt;

&lt;p&gt;The Claude Code runtime decision reflects a broader shift in how TypeScript-native tooling is evaluated. Two years ago, "build on Node.js" was the obvious default. Today, the question is more deliberate: what does this tool actually need from a runtime?&lt;/p&gt;

&lt;p&gt;For CLIs and developer tools specifically, the evaluation matrix looks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Startup latency&lt;/strong&gt;: Bun wins, meaningfully&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript execution&lt;/strong&gt;: Bun wins (no compilation step)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native module compatibility&lt;/strong&gt;: Node.js wins, but the gap is narrowing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ecosystem breadth&lt;/strong&gt;: Node.js wins (npm packages, community modules, established tooling)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bundling and distribution&lt;/strong&gt;: Bun wins for single-binary output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-running server processes&lt;/strong&gt;: Roughly equivalent; Node.js has more production mileage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anthropic's choice is defensible on every axis that matters for a CLI: fast startup, TypeScript-native execution, and streamlined distribution. The tradeoffs they accepted — opaque lockfiles, some native module risk — are manageable at their scale and usage pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical takeaways if you're building TypeScript tooling
&lt;/h2&gt;

&lt;p&gt;If you're maintaining or evaluating a TypeScript CLI or developer tool today:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit your native dependencies first.&lt;/strong&gt; List every package that includes a &lt;code&gt;.node&lt;/code&gt; binary. Check the Bun compatibility tracker or run &lt;code&gt;bun install &amp;amp;&amp;amp; bun run src/index.ts&lt;/code&gt; against your entry point and see what breaks. Native compatibility issues surface immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Bun's test runner if you switch.&lt;/strong&gt; &lt;code&gt;bun test&lt;/code&gt; is Jest-compatible (it understands &lt;code&gt;describe&lt;/code&gt;, &lt;code&gt;it&lt;/code&gt;, &lt;code&gt;expect&lt;/code&gt;) and significantly faster. It's the easiest win from a Bun migration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep &lt;code&gt;tsc --noEmit&lt;/code&gt; in CI regardless.&lt;/strong&gt; Bun's type-stripping is not type-checking. Don't let the absence of a build step create a false sense of type safety. The type checker is a separate tool; treat it as one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single-binary output is worth evaluating seriously.&lt;/strong&gt; If your CLI is distributed outside npm — as a GitHub release artifact, inside a Docker image, via Homebrew — &lt;code&gt;bun build --compile&lt;/code&gt; simplifies your distribution pipeline in ways that matter operationally.&lt;/p&gt;

&lt;p&gt;The broader lesson from Claude Code's stack choice is that runtime selection for TypeScript tooling is now a real engineering decision with real tradeoffs, not a default. Node.js is still the right answer for many contexts. For a fast, TypeScript-heavy CLI where startup latency and developer ergonomics are primary concerns, Bun is increasingly a serious contender — and Anthropic just made that case with a high-visibility production deployment.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>node</category>
      <category>javascript</category>
      <category>productivity</category>
    </item>
    <item>
      <title>React Server Components - The Mental Model Shift</title>
      <dc:creator>DimiDan</dc:creator>
      <pubDate>Mon, 20 Jul 2026 14:22:05 +0000</pubDate>
      <link>https://dev.to/dimidan/rsc-the-mental-model-shift-5efg</link>
      <guid>https://dev.to/dimidan/rsc-the-mental-model-shift-5efg</guid>
      <description>&lt;p&gt;React Server Components are not a performance trick. They are not a simpler way to do SSR. They represent a genuine renegotiation of where computation lives in a React application — and if you approach them as an incremental upgrade, you will architect yourself into corners that are painful to escape.&lt;/p&gt;

&lt;p&gt;After working through RSC adoption in production Next.js applications, I want to lay out what actually changes at the architectural level: data fetching patterns, bundle composition, state management boundaries, and how teams need to think about component ownership.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Shift did happened
&lt;/h2&gt;

&lt;p&gt;Before RSC, the React component tree was a client-side concept. Server rendering was a serialization step — you ran the same component code on the server, emitted HTML, then React rehydrated it on the client. The component tree was fundamentally one thing that happened to run in two places.&lt;/p&gt;

&lt;p&gt;RSC breaks this. The component tree now has two genuinely distinct zones with different capabilities, different execution environments, and a one-way data boundary between them. Server Components run exclusively on the server. They can access databases, filesystems, and secrets directly. They never ship to the browser. Client Components are the subset of your tree that runs in the browser — they receive serialized props from the server boundary and manage their own state and effects.&lt;/p&gt;

&lt;p&gt;The key constraint is that this boundary is &lt;strong&gt;not symmetric&lt;/strong&gt;. You can compose Client Components inside Server Components by passing them as props (including &lt;code&gt;children&lt;/code&gt;). You cannot import a Client Component into a Server Component and expect it to remain a Client Component — it gets treated as a Server Component unless explicitly marked with &lt;code&gt;'use client'&lt;/code&gt;. And you cannot import a Server Component into a Client Component at all.&lt;/p&gt;

&lt;p&gt;This asymmetry has real architectural consequences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Fetching: Colocation at Scale
&lt;/h2&gt;

&lt;p&gt;The most immediate change is to data fetching strategy. In a pre-RSC Next.js app, data fetching was pushed to page boundaries — &lt;code&gt;getServerSideProps&lt;/code&gt;, &lt;code&gt;getStaticProps&lt;/code&gt;, or client-side fetches with TanStack Query. Components deep in the tree couldn't fetch their own data without either prop-drilling from a page or reaching for a client-side solution.&lt;/p&gt;

&lt;p&gt;RSC removes that constraint. A deeply nested Server Component can fetch exactly the data it needs:&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;// app/dashboard/RecentActivity.tsx&lt;/span&gt;
&lt;span class="c1"&gt;// No 'use client' directive — this is a Server Component&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getRecentActivity&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="s1"&gt;@/lib/db/activity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;userId&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;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RecentActivity&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Props&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;activity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getRecentActivity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ul&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;activity&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="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;li&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&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="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;span&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;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/span&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt; &lt;span class="nx"&gt;dateTime&lt;/span&gt;&lt;span class="o"&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="nx"&gt;timestamp&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;formatRelative&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;timestamp&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/time&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/li&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;))}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/ul&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is genuinely useful. Components declare their own data dependencies. There is no prop-drilling waterfall through parent components that don't need the data. React can parallelize sibling server fetches.&lt;/p&gt;

&lt;p&gt;But colocation at this level has an architectural cost: &lt;strong&gt;it distributes data access logic across the component tree&lt;/strong&gt;. In large codebases, this creates pressure toward duplicated query logic, inconsistent caching behavior, and data layer concerns bleeding into rendering concerns. The discipline that kept data fetching at page or feature boundaries was a forcing function for clean separation.&lt;/p&gt;

&lt;p&gt;The practical answer is a service or repository layer that Server Components call into — not raw database queries inside JSX files. This is not a new idea, but RSC makes it newly necessary to enforce.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bundle Optimization Is Now Structural
&lt;/h2&gt;

&lt;p&gt;The bundle benefits of RSC are real but understated. It is not just that dependencies don't ship to the client. The server/client boundary &lt;strong&gt;is&lt;/strong&gt; the module graph split point.&lt;/p&gt;

&lt;p&gt;Consider a rich-text editor scenario. You have a document viewer that renders stored content, and a document editor that allows mutation. Pre-RSC, both likely live in the client bundle because rendering document content requires the same parser/renderer used for editing. With RSC, the viewer becomes a Server Component that runs the parser on the server and emits HTML. The editor, which genuinely needs client interactivity, is a Client Component. The parser library ships to zero browsers for the view-only case.&lt;/p&gt;

&lt;p&gt;This pattern — server for read paths, client boundary for write and interactive paths — maps cleanly onto a lot of B2B SaaS product structures. Dashboards, reports, document feeds: mostly reads. Editors, forms, collaborative tools: require client boundaries.&lt;/p&gt;

&lt;p&gt;What changes architecturally is that &lt;strong&gt;bundle composition is now a first-class design decision&lt;/strong&gt;, not a post-hoc optimization. When you create a component, you are deciding which execution environment it belongs to. That decision needs to be visible in your component design conventions, code review checklists, and module organization.&lt;/p&gt;

&lt;h2&gt;
  
  
  State Management Boundaries
&lt;/h2&gt;

&lt;p&gt;State management under RSC requires explicit rethinking, not just adaptation.&lt;/p&gt;

&lt;p&gt;The instinct to reach for Zustand or Redux for shared state runs into a hard constraint: stores initialize and live in the Client Component subtree. Server Components cannot access them. This is correct behavior — but it means you need a clear model for what kind of state lives where.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;URL state&lt;/strong&gt; — search params, filters, pagination — becomes a first-class citizen. Server Components can read URL parameters directly and use them to parameterize data fetches. This moves a lot of state that previously lived in client stores back to the URL, which improves shareability and reduces client-side complexity.&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;// app/invoices/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;InvoiceList&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="s1"&gt;./InvoiceList&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;SearchParams&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paid&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="s1"&gt;pending&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="s1"&gt;overdue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;page&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;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;InvoicesPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;searchParams&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="nl"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SearchParams&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;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&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;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;InvoiceList&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&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;strong&gt;Server-owned state&lt;/strong&gt; — user session, feature flags, tenant configuration — can be fetched once in a root Server Component and passed down as props, or accessed directly in any Server Component that needs it, without a context provider.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client state&lt;/strong&gt; remains appropriate for: ephemeral UI state (hover, focus, open/closed), optimistic updates, real-time collaboration state (Yjs documents, presence), and anything that responds to user input before a server round-trip.&lt;/p&gt;

&lt;p&gt;The architectural mistake to avoid is treating the client state layer as a cache for server data. TanStack Query handles this well, but under RSC the primary data loading path moves to the server. Client-side query caches are better scoped to mutations, optimistic updates, and cases where you genuinely need background refetching without a navigation event.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Client Boundary as an API Contract
&lt;/h2&gt;

&lt;p&gt;Every &lt;code&gt;'use client'&lt;/code&gt; directive is an interface declaration. The props a Client Component accepts are serializable data flowing from the server domain into the client domain. This is a meaningful constraint: you cannot pass functions (unless they are Server Actions), class instances, or non-serializable objects across this boundary.&lt;/p&gt;

&lt;p&gt;Architecturally, this forces clarity. When you design a Client Component, you are defining what the server needs to provide — the same problem as API design. What is the contract, and who owns the schema?&lt;/p&gt;

&lt;p&gt;For complex components, this pushes toward explicit prop types that mirror your data model, rather than passing rich objects and picking fields inside the component:&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;// Clear boundary contract — serializable, explicit&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;DocumentCardProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&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;title&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft&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="s1"&gt;published&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="s1"&gt;archived&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;lastEditedAt&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="c1"&gt;// ISO string, not Date — must be serializable&lt;/span&gt;
  &lt;span class="nl"&gt;collaboratorCount&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&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;function&lt;/span&gt; &lt;span class="nf"&gt;DocumentCard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;lastEditedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;collaboratorCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;DocumentCardProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// interactive behavior, local state, event handlers&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is good discipline regardless of RSC. But RSC turns violations into runtime errors rather than code smell.&lt;/p&gt;

&lt;h2&gt;
  
  
  Team and Collaboration Implications
&lt;/h2&gt;

&lt;p&gt;At the team level, RSC introduces a new axis of component ownership: server versus client. In organizations where backend and frontend responsibilities overlap — product teams owning full features end-to-end — this is largely positive. Engineers can write data access directly without an API contract mediation step.&lt;/p&gt;

&lt;p&gt;In organizations with stronger frontend/backend splits, it creates friction. Server Components that access databases or internal services blur the ownership model. You need explicit team agreements about what Server Components are allowed to call and where the service boundary sits.&lt;/p&gt;

&lt;p&gt;RFC-driven engineering practices help here. Before RSC adoption, it is worth writing down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which data sources Server Components may access directly versus through an API layer&lt;/li&gt;
&lt;li&gt;Conventions for when client boundaries should be introduced (interactive features, real-time state, third-party SDKs)&lt;/li&gt;
&lt;li&gt;How Server Actions fit into your mutation story alongside existing API routes&lt;/li&gt;
&lt;li&gt;Testing strategy — Server Components require different test infrastructure than Client Components&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What This Is Not
&lt;/h2&gt;

&lt;p&gt;RSC does not eliminate the need for a client-side data fetching library. TanStack Query remains the right tool for mutation state, optimistic updates, polling, and any data that needs to stay fresh without a navigation event.&lt;/p&gt;

&lt;p&gt;RSC does not make SSR obsolete. Streaming SSR and RSC are complementary. &lt;code&gt;Suspense&lt;/code&gt; boundaries work across both.&lt;/p&gt;

&lt;p&gt;RSC does not simplify everything. The dual execution model adds cognitive overhead. Developers need to reason about which environment their code runs in, what is available there, and where the serialization boundary sits. That cost is real and needs to be weighed against the benefits for your specific application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Lands
&lt;/h2&gt;

&lt;p&gt;RSC is the most significant change to React's execution model since hooks. It draws a clear line through the component tree and assigns different capabilities to each side. The teams that will benefit most are those building data-heavy, read-path-dominant applications — dashboards, document tools, B2B reporting surfaces — where the separation between "fetch and render" and "interact and mutate" maps cleanly to the server/client split.&lt;/p&gt;

&lt;p&gt;The architectural work is not in learning the API. It is in auditing your existing component and data fetching patterns, defining clear conventions for boundary placement, and communicating those conventions to your team before the codebase drifts into inconsistency.&lt;/p&gt;

&lt;p&gt;Done deliberately, RSC moves meaningful computation server-side in a way that is maintainable at scale. Done reactively, it produces a confusing mix of fetching strategies, inconsistent boundary placement, and components that are harder to reason about than what you had before.&lt;/p&gt;

&lt;p&gt;The paradigm is sound. The architectural investment is on you.&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
    <item>
      <title>A critical look at AI terminal coding Assistants</title>
      <dc:creator>DimiDan</dc:creator>
      <pubDate>Mon, 20 Jul 2026 14:01:09 +0000</pubDate>
      <link>https://dev.to/dimidan/a-staff-engineers-critical-look-at-ai-terminal-coding-assistants-49pm</link>
      <guid>https://dev.to/dimidan/a-staff-engineers-critical-look-at-ai-terminal-coding-assistants-49pm</guid>
      <description>&lt;p&gt;Every few weeks a new AI coding assistant lands on Hacker News with a wave of "this changes everything" comments. OpenCode is the latest terminal-first entry in this space, and the demo looks compelling. But after spending real time with it on a production TypeScript codebase, I want to offer something more useful than another glowing first-impression post: an honest accounting of where these tools help, where they hurt, and what the design decisions of tools like OpenCode reveal about the tradeoffs you're actually accepting.&lt;/p&gt;

&lt;p&gt;This isn't a hit piece. It's the kind of evaluation I'd want before spending two weeks integrating a tool into my workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  What OpenCode Is Actually Doing
&lt;/h2&gt;

&lt;p&gt;OpenCode is a terminal UI (TUI) coding assistant that wraps an LLM with file-system access, shell execution, and a diff-apply loop. You describe a change in natural language, it reads context from your repo, generates code, and applies it. The pitch is that it stays in the terminal where many engineers already live, avoiding the context-switch to a browser-based chat interface.&lt;/p&gt;

&lt;p&gt;The core loop is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You provide a prompt&lt;/li&gt;
&lt;li&gt;The tool reads relevant files (via its own heuristics or your explicit direction)&lt;/li&gt;
&lt;li&gt;The LLM generates a diff or full file replacement&lt;/li&gt;
&lt;li&gt;OpenCode applies the change and optionally runs a verification command&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is not fundamentally different from what Aider, Claude Code, or Cursor's agent mode do. The differentiation is in the UX layer, the model routing, and the assumptions baked into the context-gathering step — and those assumptions are where things get interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Context Problem Is Not Solved
&lt;/h2&gt;

&lt;p&gt;The most critical failure mode for any agentic coding tool is context selection. If the model doesn't see the right files, it will generate plausible-looking code that violates your actual constraints — wrong types, ignored abstractions, duplicated logic.&lt;/p&gt;

&lt;p&gt;OpenCode, like its peers, uses a mix of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Git history and file recency&lt;/li&gt;
&lt;li&gt;Import graph traversal&lt;/li&gt;
&lt;li&gt;Keyword and embedding similarity search&lt;/li&gt;
&lt;li&gt;Whatever you explicitly paste in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem is that in a real TypeScript monorepo — a Turborepo setup with shared packages, for example — the most important constraints often live &lt;em&gt;outside&lt;/em&gt; the file being edited. Your Zod schema in &lt;code&gt;packages/validation&lt;/code&gt;, your design token types in &lt;code&gt;packages/tokens&lt;/code&gt;, your shared component API in &lt;code&gt;packages/ui&lt;/code&gt;. These are the files that determine whether the generated code is actually correct.&lt;/p&gt;

&lt;p&gt;When I asked OpenCode to extend a form component, it generated perfectly valid TypeScript that was completely inconsistent with the validation schema the form was bound to. It looked right. It compiled. It was wrong.&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;// What OpenCode generated — valid TS, wrong contract&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FormValues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;email&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;role&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="c1"&gt;// Should be constrained to z.enum(['admin', 'member', 'viewer'])&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// What the actual Zod schema required&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&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="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&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;member&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;viewer&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;The tool didn't know about the schema because it wasn't imported in the component file — it was injected via a form context provider three layers up. This is a structural limitation. The import graph traversal is shallow, and the tool has no model of React's runtime dependency patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trust Gradient Problem
&lt;/h2&gt;

&lt;p&gt;Here's a subtler issue that doesn't get enough attention: AI coding tools create an uneven trust gradient that's dangerous for code review culture.&lt;/p&gt;

&lt;p&gt;When a junior engineer writes code, experienced reviewers bring appropriate skepticism. When an AI tool produces a clean, well-formatted diff with a tidy commit message, it &lt;em&gt;looks&lt;/em&gt; more authoritative than it is. I've watched PRs get merged faster when they're AI-assisted because the surface presentation is polished. That's backwards.&lt;/p&gt;

&lt;p&gt;AI-generated code requires &lt;em&gt;more&lt;/em&gt; scrutiny in specific areas, not less:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Edge case handling&lt;/strong&gt;: LLMs optimize for the happy path&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security boundaries&lt;/strong&gt;: Input validation, authorization checks, secrets handling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance characteristics&lt;/strong&gt;: N+1 queries, unnecessary re-renders, missing memoization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-term maintainability&lt;/strong&gt;: Generated code often solves the immediate problem in a way that accumulates tech debt&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your team hasn't explicitly recalibrated review standards for AI-assisted code, do that before adopting any of these tools at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Terminal-First Actually Wins
&lt;/h2&gt;

&lt;p&gt;There is genuine value here. Terminal-first AI assistants are meaningfully better than browser chat for a specific category of tasks: &lt;strong&gt;exploratory refactoring with tight feedback loops&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Tasks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Migrating from one API shape to another across 40 files&lt;/li&gt;
&lt;li&gt;Applying a consistent pattern (logging, error boundaries, analytics events) across many components&lt;/li&gt;
&lt;li&gt;Converting a class-based module to functional with a known target shape&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For these, the apply-verify-iterate loop in a terminal tool is faster than copy-pasting between a chat window and your editor. You stay in one context. You can run your test suite as the verification step. You see diffs without context-switching.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# This kind of workflow works well&lt;/span&gt;
opencode &lt;span class="s2"&gt;"Find all fetch() calls in src/ and wrap them with our
  httpClient utility from lib/http-client.ts, preserving error handling"&lt;/span&gt;

&lt;span class="c"&gt;# Run tests as verification&lt;/span&gt;
npm &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--watch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For mechanical, well-defined transformations with a narrow constraint space, these tools earn their keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Autonomy Dial Is Set Wrong
&lt;/h2&gt;

&lt;p&gt;Most of these tools default to too much autonomy. OpenCode will, by default, apply changes without a confirmation step if a verification command is configured and passes. That sounds reasonable until your verification command is &lt;code&gt;npm run build&lt;/code&gt; and the generated code introduces a subtle runtime bug that doesn't surface at build time.&lt;/p&gt;

&lt;p&gt;The right default is more conservative: show the diff, wait for approval, &lt;em&gt;then&lt;/em&gt; apply. Autonomy should be opt-in per task, not a global opt-out. The tools that get this right — Cursor's agent mode with explicit step approval, Claude Code with its default confirmation prompts — feel fundamentally safer on anything touching production paths.&lt;/p&gt;

&lt;p&gt;If you're evaluating OpenCode or similar tools, check the default autonomy settings before you start. Understand exactly what &lt;code&gt;--auto-apply&lt;/code&gt; and equivalent flags are doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Staff Engineer Actually Wants From These Tools
&lt;/h2&gt;

&lt;p&gt;After using several of these tools across real projects, here's what actually matters:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Respects existing conventions.&lt;/strong&gt; The tool should read your ESLint config, your component patterns, your naming conventions — and follow them without being told explicitly every time. Most tools do this poorly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Understands project-specific abstractions.&lt;/strong&gt; If you have a &lt;code&gt;useFormField&lt;/code&gt; hook or a &lt;code&gt;DataTable&lt;/code&gt; component, the tool should use them, not generate a new implementation from scratch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Doesn't hide complexity.&lt;/strong&gt; Generated code should be readable and auditable. A clever solution that works but is hard to reason about is not a win.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fails loudly, not silently.&lt;/strong&gt; If the tool can't gather enough context to be confident, it should say so. Hallucinated-but-plausible code is worse than an honest "I don't have enough context for this."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integrates with your existing feedback loop.&lt;/strong&gt; Your test suite, your type checker, your linter — not a parallel system.&lt;/p&gt;

&lt;p&gt;OpenCode scores well on the last point and poorly on the first two.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Verdict
&lt;/h2&gt;

&lt;p&gt;OpenCode is a competent implementation of a tool category that is genuinely useful for a subset of tasks. It's not a reason to overhaul your primary workflow, and it's not a substitute for understanding your codebase.&lt;/p&gt;

&lt;p&gt;Engineers who get real value from tools like this treat them as accelerators for well-scoped, mechanical work — not as autonomous collaborators on design decisions. The engineers who get burned are the ones who trust the polish of the output over the quality of the reasoning behind it.&lt;/p&gt;

&lt;p&gt;Before adopting any AI coding assistant on a production team, answer these questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What is the verification gate before AI-generated code merges?&lt;/li&gt;
&lt;li&gt;Have we updated code review expectations explicitly?&lt;/li&gt;
&lt;li&gt;Do we have a clear list of task types where we &lt;em&gt;won't&lt;/em&gt; use AI assistance — auth flows, payment logic, data migrations?&lt;/li&gt;
&lt;li&gt;Can the tool understand our project-specific abstractions, or will it work against them?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you can't answer those confidently, the tool will make things slower, not faster.&lt;/p&gt;

&lt;p&gt;The promise of AI coding assistants is real. The current implementations require more critical evaluation than they typically receive.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>typescript</category>
      <category>architecture</category>
    </item>
    <item>
      <title>GPT-5 and Convex Optimization: What the Claims Actually Mean for Engineering Tooling</title>
      <dc:creator>DimiDan</dc:creator>
      <pubDate>Mon, 20 Jul 2026 13:58:44 +0000</pubDate>
      <link>https://dev.to/dimidan/gpt-5-and-convex-optimization-what-the-claims-actually-mean-for-engineering-tooling-50ib</link>
      <guid>https://dev.to/dimidan/gpt-5-and-convex-optimization-what-the-claims-actually-mean-for-engineering-tooling-50ib</guid>
      <description>&lt;h1&gt;
  
  
  GPT-5 and Convex Optimization: What the Claims Actually Mean for Engineering Tooling
&lt;/h1&gt;

&lt;p&gt;A recent thread hit 482 points and 312 comments on the claim that GPT-5 has made meaningful progress on convex optimization problems that have been largely intractable for thirty years. The reaction split cleanly: researchers excited about specific benchmark results, engineers skeptical about practical implications, and a third camp arguing the framing was misleading from the start.&lt;/p&gt;

&lt;p&gt;All three groups have valid points. Here's an attempt to untangle them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Convex Optimization Actually Is (And Why It's Everywhere)
&lt;/h2&gt;

&lt;p&gt;Convex optimization is the class of problems where you minimize a convex function over a convex set. The key property: any local minimum is a global minimum. That makes convex problems tractable in a way that general optimization problems are not.&lt;/p&gt;

&lt;p&gt;This shows up constantly in engineering infrastructure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ML training&lt;/strong&gt; — loss surfaces are often non-convex, but large subproblems (SVM training, certain regularization schemes) are convex and solved with specialized solvers like CVXPY or Gurobi&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compiler backends&lt;/strong&gt; — register allocation and instruction scheduling have convex relaxations that inform heuristics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource allocation&lt;/strong&gt; — cloud schedulers, network routing, and capacity planning often reduce to linear or quadratic programs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Financial modeling&lt;/strong&gt; — portfolio optimization under constraints is a textbook convex problem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The "30-year gap" framing refers to open problems around &lt;em&gt;scalability and solver efficiency&lt;/em&gt; — specifically, the gap between what interior-point methods can handle in theory and what's practical at production scale.&lt;/p&gt;

&lt;p&gt;Solvers like MOSEK and SCS are excellent. They're also slow at scale, sensitive to problem conditioning, and require expert formulation. That last point is the friction that actually matters for engineering teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the GPT-5 Results Actually Show
&lt;/h2&gt;

&lt;p&gt;The specific claims center on GPT-5's ability to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Correctly formulate optimization problems from natural-language descriptions&lt;/li&gt;
&lt;li&gt;Identify when a problem has convex structure that allows efficient solving&lt;/li&gt;
&lt;li&gt;Generate CVXPY or similar solver code that compiles and runs correctly&lt;/li&gt;
&lt;li&gt;In some benchmarks, suggest reformulations that meaningfully improve solver performance&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is not "GPT-5 solved convex optimization." It's closer to: GPT-5 has become a competent co-pilot for the &lt;em&gt;formulation&lt;/em&gt; step, which is where most engineering time actually goes.&lt;/p&gt;

&lt;p&gt;To make this concrete — if you're building a job scheduler and need to minimize total latency subject to resource constraints, writing that as a proper LP or QP has always required knowing the vocabulary: decision variables, objective function, constraint matrices. That knowledge barrier is real. Most backend engineers reach for heuristics not because an optimal formulation wouldn't be better, but because they don't want to spend three days reading Boyd and Vandenberghe.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# What you used to need domain knowledge to write:
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cvxpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cp&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="c1"&gt;# Job scheduling: minimize makespan across n jobs, m machines
&lt;/span&gt;&lt;span class="n"&gt;n_jobs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_machines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="n"&gt;processing_times&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_jobs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_machines&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Variable&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;n_jobs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_machines&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;makespan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Variable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;constraints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;cp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# each job assigned to exactly one machine
&lt;/span&gt;    &lt;span class="n"&gt;makespan&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;cp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;multiply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;processing_times&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&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="n"&gt;objective&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Minimize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;makespan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;problem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Problem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;objective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;constraints&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;solve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GPT-5 can now generate that correctly from a plain-English description of the scheduling problem. More importantly, it can flag when you've accidentally written a non-convex constraint that will cause the solver to fail silently or return garbage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Skepticism in the Thread Is Legitimate
&lt;/h2&gt;

&lt;p&gt;The 312-comment thread wasn't just noise. The pushback clustered around several real concerns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Benchmark performance doesn't transfer to production problems."&lt;/strong&gt; True. The benchmarks use well-conditioned, textbook-scale problems. Real scheduling problems have messy side constraints, integer variables, and numerical conditioning issues that stress even expert formulations. GPT-5 generating plausible-looking CVXPY code is not the same as that code solving your actual problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"This conflates formulation assistance with algorithmic progress."&lt;/strong&gt; Also true. The headline "closing a 30-year gap" implies progress on solver algorithms — faster interior-point methods, better preconditioning, novel dual decompositions. That's not what's being described. What's being described is better tooling around &lt;em&gt;using&lt;/em&gt; existing solvers. Valuable, but a different category of claim.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"LLMs hallucinate constraints."&lt;/strong&gt; This is the one that should give you pause before deploying any of this. GPT-4 can generate convex-looking constraint sets that are subtly non-convex — the kind of error that doesn't throw an exception, but silently returns a wrong answer. GPT-5 is better at catching these, but "better" doesn't mean "safe to run unsupervised in a production resource allocator."&lt;/p&gt;

&lt;p&gt;The honest framing: this is a significant improvement in the &lt;em&gt;accessibility&lt;/em&gt; of optimization tooling, not a breakthrough in optimization theory.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Actually Changes for Engineering Teams
&lt;/h2&gt;

&lt;p&gt;If you're building backend infrastructure, developer tooling, or any system that currently uses heuristics because "setting up a proper solver seemed like too much work," the calculus has shifted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The formulation barrier is lower.&lt;/strong&gt; You can describe your problem in prose, get a working CVXPY sketch, and iterate from there. That's genuinely useful even if you still need to validate the output carefully.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debugging solver failures is faster.&lt;/strong&gt; Infeasibility and numerical issues in optimization problems have always been hard to diagnose. Having a model that can look at your problem and say "this constraint is likely causing infeasibility because X" cuts debugging time substantially.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The integration surface with existing infrastructure is unchanged.&lt;/strong&gt; CVXPY still calls MOSEK or SCS. Solver performance characteristics are the same. If your problem is too large for current solvers, GPT-5 doesn't fix that. You still need decomposition, approximation, or a different approach.&lt;/p&gt;

&lt;p&gt;For teams building scheduling systems, ML infrastructure, or resource allocation layers — say, a Node.js/TypeScript backend calling out to a Python optimization service — the practical win is in prototyping speed and reducing the expertise required to reach a working formulation.&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;// A backend service that now has a shorter path to proper optimization&lt;/span&gt;
&lt;span class="c1"&gt;// instead of a hand-rolled heuristic&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;scheduleJobs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;machines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Machine&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Assignment&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="c1"&gt;// Previously: greedy heuristic because writing the LP felt like too much&lt;/span&gt;
  &lt;span class="c1"&gt;// Now: call a Python optimization service with a properly formulated problem&lt;/span&gt;
  &lt;span class="c1"&gt;// generated and validated with AI assistance&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formulation&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;optimizationService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;solve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;linear_program&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;objective&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;minimize_makespan&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;machines&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;formulation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assignments&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;h2&gt;
  
  
  Where to Be Careful
&lt;/h2&gt;

&lt;p&gt;A few concrete cautions before building this into critical infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Always verify the problem class.&lt;/strong&gt; If the model says your problem is convex, check it. A non-convex problem handed to a convex solver will either fail noisily or return a locally optimal point far from the global optimum. In a scheduler or resource allocator, that has real consequences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Numerical conditioning still requires human attention.&lt;/strong&gt; LLMs generate well-scaled constraint matrices for textbook problems. Your production problem has characteristics the model hasn't seen. Check condition numbers. Know your solver's tolerance settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't skip domain expert review.&lt;/strong&gt; The formulation step is where domain knowledge matters most — not just mathematical correctness, but whether the optimization objective actually captures what you care about. GPT-5 can give you a mathematically valid LP that optimizes the wrong thing because it misunderstood a business constraint.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Opportunity
&lt;/h2&gt;

&lt;p&gt;The real engineering opportunity here isn't "replace your solver with GPT-5." It's closer to: a class of infrastructure problems previously gated behind specialized knowledge is now more accessible to generalist engineers.&lt;/p&gt;

&lt;p&gt;That has compounding effects. Teams that previously used greedy heuristics because the optimization setup cost was too high can now prototype proper formulations. Those prototypes, even if they need expert refinement, start from a better place than a hand-rolled approximation.&lt;/p&gt;

&lt;p&gt;For developer tooling specifically — compilers, build systems, deployment schedulers — this opens up experimentation with optimization-based approaches that teams routinely deprioritized because of implementation cost.&lt;/p&gt;

&lt;p&gt;The 30-year gap in optimization theory hasn't closed. But the gap between optimization theory and something an engineering team can actually use got meaningfully smaller. That's a more modest claim than the headline suggested — but it's a real one, and it's worth taking seriously.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Fintech Frontend Engineering and Financial Services</title>
      <dc:creator>DimiDan</dc:creator>
      <pubDate>Sat, 27 Jun 2026 20:10:21 +0000</pubDate>
      <link>https://dev.to/dimidan/fintech-frontend-engineering-what-you-need-to-know-before-building-in-financial-services-26b0</link>
      <guid>https://dev.to/dimidan/fintech-frontend-engineering-what-you-need-to-know-before-building-in-financial-services-26b0</guid>
      <description>&lt;p&gt;Most frontend engineers entering fintech underestimate how much the domain changes what &lt;em&gt;good&lt;/em&gt; looks like. It's not just stricter security requirements or more complex forms. The regulatory, compliance, and payments context fundamentally reshapes how you architect UI, how you reason about state, how you handle errors, and what "done" means.&lt;/p&gt;

&lt;p&gt;This post is a reference for frontend engineers — particularly those operating at staff level, where you're expected to contribute credibly beyond UI concerns — moving into or deeper into fintech. It covers the conceptual terrain: compliance constraints, payments flows, and the architectural patterns that emerge from them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Domain Fluency Matters at the Frontend Layer
&lt;/h2&gt;

&lt;p&gt;At staff level, you're expected to make architectural decisions that won't rot under you. In fintech, those decisions carry regulatory surface area. A poorly designed state machine for a payments flow isn't just a UX bug — it can result in double charges, incomplete settlements, or audit failures.&lt;/p&gt;

&lt;p&gt;Frontend engineers who treat fintech as "the same job with stricter validation" tend to build systems that handle happy paths correctly but crack under the domain's actual failure modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Idempotency violations from optimistic UI updates&lt;/li&gt;
&lt;li&gt;Race conditions between transaction state and UI state&lt;/li&gt;
&lt;li&gt;Accessibility failures that violate regulatory obligations (WCAG is a legal requirement in financial services in several jurisdictions)&lt;/li&gt;
&lt;li&gt;Audit trail gaps caused by insufficient event logging at the client layer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Domain fluency means you can anticipate these failure modes &lt;em&gt;before&lt;/em&gt; they're filed as incidents.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Compliance Layer Is an Architectural Constraint
&lt;/h2&gt;

&lt;p&gt;In most product domains, compliance is a checklist you run through late in a feature cycle. In fintech, it's a first-class architectural input.&lt;/p&gt;

&lt;h3&gt;
  
  
  KYC and KYB Flows
&lt;/h3&gt;

&lt;p&gt;Know Your Customer (KYC) and Know Your Business (KYB) verification flows are often the first complex frontend engineering problem you'll encounter. They look like multi-step forms, but they behave like state machines with external dependencies.&lt;/p&gt;

&lt;p&gt;A KYC flow typically involves:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Document collection (government ID, proof of address)&lt;/li&gt;
&lt;li&gt;Submission to a third-party verification provider (e.g., Persona, Onfido, Jumio)&lt;/li&gt;
&lt;li&gt;Asynchronous verification (taking minutes to days)&lt;/li&gt;
&lt;li&gt;Status polling or webhook-triggered state transitions&lt;/li&gt;
&lt;li&gt;Remediation loops for rejected submissions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The frontend architecture needs to handle the async nature explicitly. A naive implementation that treats verification as a synchronous form submission will fail badly. You need a durable state model:&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;KYCStatus&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;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not_started&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;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;documents_pending&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;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submitted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;submittedAt&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;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;under_review&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;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;approved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;approvedAt&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;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rejected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;reason&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;retryAllowed&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="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;manual_review&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 discriminated union pattern forces every rendering path to handle every state explicitly. When the backend team adds a new status, TypeScript surfaces every unhandled case in the UI. In regulated flows, "we missed a status" is not an acceptable postmortem entry.&lt;/p&gt;

&lt;h3&gt;
  
  
  Audit Trails and Immutability
&lt;/h3&gt;

&lt;p&gt;Regulatory frameworks — PCI-DSS, SOX, GDPR, FCA rules — often require that financial actions be attributable, timestamped, and non-repudiable. This has direct implications for how you design frontend event logging.&lt;/p&gt;

&lt;p&gt;Client-side events that touch payment initiation, consent recording, or document submission should be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Emitted as structured, typed events&lt;/li&gt;
&lt;li&gt;Correlated with a server-side trace ID&lt;/li&gt;
&lt;li&gt;Stored immutably (no update or delete semantics at the log layer)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not analytics. It's an audit trail. Design it accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Payments Flows: The State Machine Problem
&lt;/h2&gt;

&lt;p&gt;A payments flow is a distributed state machine with multiple participants: the user, your frontend, your backend, and a payments processor (Stripe, Adyen, Braintree). The frontend's job is to accurately reflect state transitions that it does not fully control.&lt;/p&gt;

&lt;h3&gt;
  
  
  Idempotency and Optimistic UI
&lt;/h3&gt;

&lt;p&gt;Optimistic UI — updating local state before server confirmation — is a standard pattern for improving perceived performance. In payments, it's dangerous without careful design.&lt;/p&gt;

&lt;p&gt;Consider a "Pay Now" button. If a user taps it and the network is slow, they may tap again. Without idempotency keys on the backend &lt;em&gt;and&lt;/em&gt; request deduplication on the frontend, you risk two charges.&lt;/p&gt;

&lt;p&gt;The correct pattern:&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;const&lt;/span&gt; &lt;span class="nx"&gt;usePaymentSubmit&lt;/span&gt; &lt;span class="o"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&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;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idle&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="s1"&gt;submitting&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="s1"&gt;success&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="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idle&lt;/span&gt;&lt;span class="dl"&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;submit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;paymentDetails&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PaymentDetails&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submitting&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&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="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submitting&lt;/span&gt;&lt;span class="dl"&gt;'&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;initiatePayment&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;paymentDetails&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&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="p"&gt;});&lt;/span&gt;
      &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&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="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="c1"&gt;// Do NOT reset idempotencyKey — allow retry with the same key&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;submit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&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 idempotency key persists across retries intentionally. If the first request succeeded but the response was lost in transit, a retry with the same key returns the original result rather than creating a second transaction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Terminal vs. Non-Terminal States
&lt;/h3&gt;

&lt;p&gt;Transaction states divide into terminal (no further transitions possible) and non-terminal (may still change). &lt;code&gt;SETTLED&lt;/code&gt; and &lt;code&gt;REFUNDED&lt;/code&gt; are terminal. &lt;code&gt;PENDING&lt;/code&gt; and &lt;code&gt;PROCESSING&lt;/code&gt; are not.&lt;/p&gt;

&lt;p&gt;UI that treats a non-terminal state as final is incorrect. A payment showing &lt;code&gt;PROCESSING&lt;/code&gt; needs a polling mechanism or a WebSocket subscription to eventually converge to &lt;code&gt;SETTLED&lt;/code&gt; or &lt;code&gt;FAILED&lt;/code&gt;. Users who close the browser and return need to see the current state, not a cached intermediate state.&lt;/p&gt;

&lt;p&gt;Your state management layer needs to distinguish between &lt;em&gt;local UI state&lt;/em&gt; and &lt;em&gt;remote transaction state&lt;/em&gt;, and it needs a reconciliation mechanism:&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;// TanStack Query handles staleness and refetching cleanly&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;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transaction&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&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="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;transaction&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;transactionId&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="nf"&gt;fetchTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transactionId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;refetchInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&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="c1"&gt;// Stop polling once we reach a terminal state&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;terminalStates&lt;/span&gt; &lt;span class="o"&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;settled&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;failed&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;refunded&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;cancelled&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="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;terminalStates&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="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&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="mi"&gt;2000&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;h2&gt;
  
  
  Regulatory UI Obligations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Accessibility Is Not Optional
&lt;/h3&gt;

&lt;p&gt;In the UK, the Equality Act 2010 requires that financial services be accessible. In the EU, the European Accessibility Act came into full force in June 2025. In the US, the DOJ has increasingly cited WCAG 2.1 AA as the standard for ADA compliance in financial services litigation.&lt;/p&gt;

&lt;p&gt;WCAG compliance in fintech is a legal obligation, not a nice-to-have. A payment form with poor focus management, missing ARIA labels on error states, or keyboard traps isn't just a bad experience — it's a liability.&lt;/p&gt;

&lt;p&gt;For complex flows (multi-step forms, modal confirmation dialogs, dynamic error states), you need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manage focus programmatically on step transitions&lt;/li&gt;
&lt;li&gt;Announce dynamic content changes via &lt;code&gt;aria-live&lt;/code&gt; regions&lt;/li&gt;
&lt;li&gt;Associate error messages with their inputs via &lt;code&gt;aria-describedby&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Test with actual screen readers, not just automated scanners&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Consent and Disclosure UI
&lt;/h3&gt;

&lt;p&gt;Many financial actions require explicit, recorded consent: terms of service acceptance, fee disclosures, risk warnings. The UI patterns here are legally significant.&lt;/p&gt;

&lt;p&gt;A pre-checked checkbox, an auto-dismissing modal, or a disclosure hidden below the fold can each invalidate consent in regulatory terms. These components need compliance review, not just product review.&lt;/p&gt;

&lt;p&gt;When building consent components, treat the consent event as a domain event with a typed payload:&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;ConsentEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;consent_recorded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;userId&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;consentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;terms_of_service&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="s1"&gt;fee_disclosure&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="s1"&gt;risk_warning&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;documentVersion&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;timestamp&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="c1"&gt;// ISO 8601&lt;/span&gt;
  &lt;span class="nl"&gt;ipAddress&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="c1"&gt;// captured server-side&lt;/span&gt;
  &lt;span class="nl"&gt;userAgent&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend initiates this event; the backend records it immutably.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Patterns That Emerge from Fintech Constraints
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Strict Data Flow and Validation
&lt;/h3&gt;

&lt;p&gt;Financial data has tight schemas. An amount field that accepts &lt;code&gt;string&lt;/code&gt; when it should be &lt;code&gt;number&lt;/code&gt; in minor currency units, or an ambiguously formatted date, can cause downstream failures in payment processing. Use Zod (or an equivalent schema library) to validate API responses at the boundary:&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;z&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="s1"&gt;zod&lt;/span&gt;&lt;span class="dl"&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;TransactionSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&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="nx"&gt;z&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="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;amountMinorUnits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;positive&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&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="nf"&gt;length&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;// ISO 4217&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&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;processing&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;settled&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;failed&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;refunded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
  &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&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="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Transaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;TransactionSchema&lt;/span&gt;&lt;span class="o"&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;This validation belongs at the API client layer, not scattered across components. When a backend team changes a field type, you catch it at the boundary — not in a production error report.&lt;/p&gt;

&lt;h3&gt;
  
  
  Feature Flagging and Progressive Rollouts
&lt;/h3&gt;

&lt;p&gt;Financial features can't be safely rolled back mid-transaction. A user who has started a payment flow under version A of your app cannot safely complete it under version B if the flow structure has changed. Feature flags and cohort-based rollouts are therefore critical infrastructure, not optional.&lt;/p&gt;

&lt;p&gt;Design new payment flows as entirely separate routes or experiences, keeping the old flow live until the rollout is complete and all in-flight transactions have resolved.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sensitive Data Handling on the Client
&lt;/h3&gt;

&lt;p&gt;Card numbers, bank account details, and government ID numbers should never pass through your application's JavaScript. Use iframe-based tokenization (Stripe Elements, Adyen Web Components) so sensitive data travels directly from the browser to the processor. Your app receives a token, not the raw data.&lt;/p&gt;

&lt;p&gt;This isn't just a security best practice — it substantially reduces your PCI-DSS compliance scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Means for a Frontend Engineer in Practice
&lt;/h2&gt;

&lt;p&gt;Building in fintech means accepting that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every flow has a failure mode that matters more than the happy path.&lt;/strong&gt; Design for &lt;code&gt;settled&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt;, and &lt;code&gt;pending&lt;/code&gt;, not just success.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State machines are load-bearing structures.&lt;/strong&gt; Informal state management — boolean flags, nested conditionals — will break in a domain with this many states and transitions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance constraints are design constraints.&lt;/strong&gt; Engage with them early, not at the end of a sprint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The TypeScript type system is a compliance tool.&lt;/strong&gt; Exhaustive unions and strict validation at API boundaries catch domain errors before they become incidents.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Frontend engineers who understand the domain can challenge product decisions, identify compliance risks in designs, and write code that reflects the actual semantics of financial operations. That's the difference between owning a UI layer and being a full contributor to a fintech product.&lt;/p&gt;

&lt;p&gt;The investment in domain knowledge pays back quickly — and in fintech, the cost of &lt;em&gt;not&lt;/em&gt; making it is measured in real money.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>frontend</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Prefer duplication over the wrong abstraction</title>
      <dc:creator>DimiDan</dc:creator>
      <pubDate>Tue, 23 Jun 2026 03:18:00 +0000</pubDate>
      <link>https://dev.to/dimidan/prefer-duplication-over-the-wrong-abstraction-1kf1</link>
      <guid>https://dev.to/dimidan/prefer-duplication-over-the-wrong-abstraction-1kf1</guid>
      <description>&lt;p&gt;Sandi Metz wrote &lt;a href="https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction" rel="noopener noreferrer"&gt;The Wrong Abstraction&lt;/a&gt; in 2016. It keeps resurfacing — in high-scoring HN threads, architecture Slack channels, and code review comments — whenever a team is staring at a function that started as a clean DRY consolidation and has since acquired seven parameters, three feature flags, and a comment that says &lt;code&gt;# don't touch this&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The core argument is simple: duplication is far cheaper than the wrong abstraction. Once you've built the wrong abstraction, you're not starting from a clean slate — you're inheriting someone else's sunk cost, and every new requirement gets jammed into a shape that doesn't quite fit.&lt;/p&gt;

&lt;p&gt;I've spent a lot of time at the intersection where this matters most: design systems, editor foundations, and cross-team API contracts. These are exactly the contexts where the abstraction temptation is strongest — and where getting it wrong is most expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the wrong abstraction is so easy to create
&lt;/h2&gt;

&lt;p&gt;Abstractions don't usually start wrong. They start reasonable.&lt;/p&gt;

&lt;p&gt;You have two components that render a card. They share 80% of their structure. You extract a &lt;code&gt;Card&lt;/code&gt; component, parameterize the differences, and ship it. Six months later, &lt;code&gt;Card&lt;/code&gt; has a &lt;code&gt;variant&lt;/code&gt; prop with eight values, a &lt;code&gt;withBorder&lt;/code&gt; flag, a &lt;code&gt;compact&lt;/code&gt; prop, a &lt;code&gt;headerSlot&lt;/code&gt;, and a &lt;code&gt;suppressDefaultPadding&lt;/code&gt; escape hatch added by someone who needed the component to do something the abstraction never anticipated.&lt;/p&gt;

&lt;p&gt;The problem isn't that the original extraction was wrong. The problem is what happened next: every new use case was treated as a reason to extend the existing abstraction rather than a signal to question whether the abstraction still fit.&lt;/p&gt;

&lt;p&gt;Metz identifies the specific failure mode: &lt;em&gt;the abstraction is not the thing itself, it's a theory about the thing&lt;/em&gt;. When the theory is wrong, new data doesn't correct it — it gets explained away by adding parameters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost of inline duplication is lower than it looks
&lt;/h2&gt;

&lt;p&gt;When two components share markup, the instinct is to extract immediately. But consider what you actually know at extraction time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You know what the two existing callers need.&lt;/li&gt;
&lt;li&gt;You don't know what the third caller will need.&lt;/li&gt;
&lt;li&gt;You don't know which parts of the shared structure are incidentally similar versus structurally equivalent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Incidental similarity is the trap. Two things can look identical for completely different reasons. A &lt;code&gt;StatusBadge&lt;/code&gt; and a &lt;code&gt;CategoryTag&lt;/code&gt; might render the same pill shape today. That doesn't mean they'll evolve together. Extracting them into a shared &lt;code&gt;Pill&lt;/code&gt; component couples their evolution even if their domains have nothing in common.&lt;/p&gt;

&lt;p&gt;Duplication, in this case, is not laziness — it's preserving optionality. Each component can change shape independently when its domain requirements diverge, which they will.&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="c1"&gt;// Two components that look identical today&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;StatusBadge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OrderStatus&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`badge badge--&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;STATUS_LABELS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;status&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;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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CategoryTag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProductCategory&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`badge badge--&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;CATEGORY_LABELS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;category&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;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="c1"&gt;// Six months later, StatusBadge needs a tooltip and an ARIA live region.&lt;/span&gt;
&lt;span class="c1"&gt;// CategoryTag needs a remove button and a count indicator.&lt;/span&gt;
&lt;span class="c1"&gt;// The shared abstraction would now be a liability.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you'd extracted a &lt;code&gt;Pill&lt;/code&gt; component on day one, you'd now be untangling it. Instead, two independent components diverge cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  When shared primitives are genuinely correct
&lt;/h2&gt;

&lt;p&gt;None of this means "never abstract." The argument is about &lt;em&gt;wrong&lt;/em&gt; abstractions, not abstractions generally. There's a category of shared code that genuinely belongs together: primitives that are structurally equivalent by design, not incidentally similar by coincidence.&lt;/p&gt;

&lt;p&gt;In a design system, a &lt;code&gt;Button&lt;/code&gt; component is a real abstraction. It exists because button behavior, accessibility semantics, focus management, and visual consistency are supposed to be uniform across every surface. The shared implementation isn't an accident — it's the point. When the design system updates the focus ring to meet WCAG 2.2 criteria, you want every button in the product to update from one change.&lt;/p&gt;

&lt;p&gt;The test for a real abstraction is whether its callers are &lt;em&gt;supposed&lt;/em&gt; to be coupled. If coupling them is a feature — consistent behavior, enforced constraints, centralized correctness — the abstraction earns its place. If coupling them is just an artifact of current visual similarity, it's a liability.&lt;/p&gt;

&lt;p&gt;A useful heuristic for architecture reviews:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Does this abstraction encode a rule, or does it encode a coincidence?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A &lt;code&gt;Button&lt;/code&gt; encodes a rule: interactive elements with this semantic role should behave this way. A &lt;code&gt;Pill&lt;/code&gt; extracted from &lt;code&gt;StatusBadge&lt;/code&gt; and &lt;code&gt;CategoryTag&lt;/code&gt; encodes a coincidence: these two things look the same right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The parameter accumulation signal
&lt;/h2&gt;

&lt;p&gt;You rarely see a wrong abstraction clearly at creation time. The signal appears later, in how the abstraction responds to pressure.&lt;/p&gt;

&lt;p&gt;Right abstractions absorb new requirements gracefully. You add a new button variant: &lt;code&gt;&amp;lt;Button variant="ghost"&amp;gt;&lt;/code&gt;. The existing API handles it cleanly. The abstraction was modeling something real, and the new case fits the model.&lt;/p&gt;

&lt;p&gt;Wrong abstractions resist new requirements. You need the card component to suppress its default padding in one specific context. There's no clean way to express that, so you add &lt;code&gt;suppressDefaultPadding={true}&lt;/code&gt;. The prop name itself is a confession — it's not modeling a concept, it's punching an escape hatch through the existing model.&lt;/p&gt;

&lt;p&gt;Parameter accumulation — especially boolean flags or parameters that only matter to one caller — is the clearest signal that an abstraction has outlived its theory.&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;// This function signature is a warning sign&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;formatUserName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;includeTitle&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;shortForm&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;uppercaseLastName&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="c1"&gt;// added for the export feature&lt;/span&gt;
    &lt;span class="nl"&gt;omitMiddleName&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="c1"&gt;// added for the badge component&lt;/span&gt;
    &lt;span class="nl"&gt;legalFormat&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="c1"&gt;// added for contract generation&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function is doing five jobs. It's no longer a formatting function — it's a dispatch table that routes to five different formatting strategies based on flags. The right move is to inline the relevant logic at each call site, or to create five clearly-named functions that each do one thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The recovery path: inline before you re-extract
&lt;/h2&gt;

&lt;p&gt;Metz's prescription is specific: when you recognize a wrong abstraction, don't refactor it — &lt;em&gt;inline it&lt;/em&gt;. Copy the abstraction's code back to each call site, restore the parameters to their concrete values, delete the dead branches, and see what you actually have.&lt;/p&gt;

&lt;p&gt;This is uncomfortable. It feels like moving backward. But what you get after inlining is the truth: the actual logic each caller needs, without the mediation of a theory that no longer fits.&lt;/p&gt;

&lt;p&gt;From that position, you can see whether a new abstraction is warranted. Sometimes the call sites look very different after inlining — which tells you the old abstraction was hiding real divergence. Sometimes they look nearly identical — which means a better abstraction is now obvious, because you're working from real requirements rather than accumulated historical ones.&lt;/p&gt;

&lt;p&gt;I've done this with editor extension configurations, design token resolution logic, and API response normalization layers. Every time, the inline step was the uncomfortable-but-necessary precondition for getting the abstraction right.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cross-team dimension
&lt;/h2&gt;

&lt;p&gt;At staff level, wrong abstractions have a social dimension that makes them harder to fix. When an abstraction is owned by one team and consumed by three others, inlining it requires coordination. The owning team has to be willing to let go of something they built. The consuming teams have to accept temporary duplication. Everyone involved has to resist the pull toward "let's just add another parameter."&lt;/p&gt;

&lt;p&gt;This is where architecture reviews and RFCs earn their keep. The time to debate whether an abstraction is modeling the right thing is &lt;em&gt;before&lt;/em&gt; three teams build on top of it — not after. A good RFC process forces the question: is this abstraction encoding a rule that should govern all callers, or is it encoding one team's current needs in a shape that will constrain everyone else later?&lt;/p&gt;

&lt;p&gt;The answer isn't always to avoid the shared abstraction. Sometimes the shared primitive is genuinely the right call. But asking the question explicitly, with concrete examples of what future callers might need, catches a lot of wrong abstractions before they get adopted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual takeaway
&lt;/h2&gt;

&lt;p&gt;Metz's essay persists because it names something that's easy to see in retrospect and hard to see in the moment. The wrong abstraction feels like good engineering when you create it. It reduces line count, eliminates repetition, and looks clean. The cost shows up later, incrementally, as each new requirement gets shoe-horned in.&lt;/p&gt;

&lt;p&gt;The discipline it requires is specific: resist the extraction until you understand what the shared structure actually represents. Duplicate freely when the similarity is incidental. Extract confidently when the abstraction encodes a real rule. And when you inherit something that's accumulated enough parameters to be unreadable, have the resolve to inline it before you try to fix it.&lt;/p&gt;

&lt;p&gt;Duplication is recoverable. The wrong abstraction is a debt that compounds.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>typescript</category>
      <category>frontend</category>
      <category>designsystem</category>
    </item>
    <item>
      <title>When an AI Agent joins your Yjs room</title>
      <dc:creator>DimiDan</dc:creator>
      <pubDate>Sun, 21 Jun 2026 03:18:14 +0000</pubDate>
      <link>https://dev.to/dimidan/when-an-ai-agent-joins-your-yjs-room-three-assumptions-break-50h8</link>
      <guid>https://dev.to/dimidan/when-an-ai-agent-joins-your-yjs-room-three-assumptions-break-50h8</guid>
      <description>&lt;p&gt;Wiring an LLM as a first-class Yjs peer is architecturally sound — but it invalidates three silent assumptions your collaboration stack already makes about peer symmetry: throughput, undo ownership, and presence cadence.&lt;/p&gt;




&lt;p&gt;You've tuned a Yjs provider under real collaborative load. You know the feeling before you can name it — one heavy client starts lagging the room, presence updates stutter, and you end up adding a debounce somewhere and calling it done.&lt;/p&gt;

&lt;p&gt;Now imagine that client generates text at 3,000 words per minute, never goes offline, and has its own awareness cursor.&lt;/p&gt;

&lt;p&gt;That's not a sidebar feature. That's a new class of peer, and your collaboration architecture wasn't designed for it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Demo Is Real — But It Skips the Hard Parts
&lt;/h2&gt;

&lt;p&gt;In April 2026, a working demo wired an LLM as a genuine server-side Yjs document peer — same transport as the human editors, same CRDT, its own awareness state. The implementation uses &lt;code&gt;y-prosemirror&lt;/code&gt; and the standard awareness protocol directly. If you've shipped TipTap collaboration, you already have every dependency it needs.&lt;/p&gt;

&lt;p&gt;The architecture is correct. Making the agent a server-side peer — rather than a client-side bolt-on posting diffs over a REST endpoint — gives you one convergence model instead of two, real presence semantics for the agent, and a clean separation between the LLM streaming layer and the document state layer.&lt;/p&gt;

&lt;p&gt;But the demo establishes the peer model. It doesn't stress-test what happens to your existing assumptions once that peer is running.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Silent Assumption Every CRDT Implementation Makes
&lt;/h2&gt;

&lt;p&gt;Here it is — the assumption baked into the Yjs awareness protocol, the undo manager, and your backpressure strategy, the one nobody wrote down because it was always true until now:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All peers produce operations at roughly human speed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not identical speed. Human typists vary. But they land in the same order of magnitude. The entire design space — how often you broadcast awareness, how you scope undo history, whether you need per-peer rate limiting at the application layer — rests on that implicit contract.&lt;/p&gt;

&lt;p&gt;An AI agent at 1,000–4,000 words per minute is 25–100× outside that range. It doesn't just stress your transport. It invalidates the mental model.&lt;/p&gt;

&lt;p&gt;Here's what actually breaks.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Backpressure: The Chokepoint You Don't Have
&lt;/h2&gt;

&lt;p&gt;A central OT server can throttle any client trivially — it's the authority, it controls the queue. A CRDT peer model has no natural chokepoint. That's the tradeoff you accepted when you chose Yjs, and it's usually fine because human peers self-limit.&lt;/p&gt;

&lt;p&gt;An agent peer doesn't self-limit. Left unrestricted, its &lt;code&gt;doc.transact()&lt;/code&gt; calls will flood the sync cycle and starve human-paced operations of their share of the convergence window. This is write starvation — the same class of problem as database concurrency — and it manifests as cursor lag and dropped presence updates for everyone else in the room.&lt;/p&gt;

&lt;p&gt;The fix doesn't belong at the transport layer. It belongs between the LLM's streaming output and the Yjs document write:&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;// Token bucket between LLM stream and Yjs write&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agentBucket&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;TokenBucket&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// max queued ops&lt;/span&gt;
  &lt;span class="na"&gt;refillRate&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="c1"&gt;// ops per 100ms — keeps agent below human starvation threshold&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;llmStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;agentBucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;ydoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transact&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;ytext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;insertionPoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;agentOrigin&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 numbers are illustrative — tune them against your provider and room size. The point is that the rate limit lives at the application layer, scoped to the agent's origin, so human operations always get a guaranteed share of the convergence window regardless of how fast the model is generating.&lt;/p&gt;

&lt;p&gt;This is also where the CRDT-vs-OT debate gets re-litigated in 2026. The peer model is still right for human collaboration. For AI agents specifically, you're adding a lightweight central constraint back in — not for correctness, but for fairness.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Undo History: The Origin Problem You Probably Already Have
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;y-undomanager&lt;/code&gt; scopes undo history by origin. This is correct behavior and it's documented. But "correct" and "deliberate" aren't the same thing.&lt;/p&gt;

&lt;p&gt;If the agent's operations share an origin with the user's, &lt;code&gt;Ctrl+Z&lt;/code&gt; becomes a coin flip. If the agent gets its own origin — which it should — you now have a second question: should user-facing undo ever surface agent operations, and if so, in what order relative to the user's own history?&lt;/p&gt;

&lt;p&gt;There's no universal answer, but there is a clear principle: give the agent a separate &lt;code&gt;UndoManager&lt;/code&gt; with its own &lt;code&gt;trackedOrigins&lt;/code&gt;, and expose agent-undo as a distinct UI affordance, not the default &lt;code&gt;Ctrl+Z&lt;/code&gt; path.&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;const&lt;/span&gt; &lt;span class="nx"&gt;userUndoManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UndoManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ytext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;trackedOrigins&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;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;userOrigin&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;agentUndoManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UndoManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ytext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;trackedOrigins&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;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;agentOrigin&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// User's Ctrl+Z only touches userUndoManager.&lt;/span&gt;
&lt;span class="c1"&gt;// "Reject AI suggestion" calls agentUndoManager.undo().&lt;/span&gt;
&lt;span class="c1"&gt;// These stacks don't interfere.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same design decision you face when adding comment marks or tracked-change marks to ProseMirror — marks that &lt;em&gt;describe&lt;/em&gt; content rather than &lt;em&gt;being&lt;/em&gt; content need a separate lifecycle from marks the user controls directly. The agent peer is the document-level version of that same pattern.&lt;/p&gt;

&lt;p&gt;If you've ever had a user accidentally undo a comment thread someone else left, you've already felt this problem. The fix is the same: make the ownership boundary explicit at the manager level, not implicit in a &lt;code&gt;if (origin === agentOrigin) return&lt;/code&gt; buried in a command handler.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Presence and Awareness: Coalesce or Drown
&lt;/h2&gt;

&lt;p&gt;The awareness protocol was designed for human-paced cursor updates. A few broadcasts per second per peer is normal; the rendering layer handles it fine.&lt;/p&gt;

&lt;p&gt;An agent generating 3,000 wpm produces position changes at a rate no human can visually process. Broadcasting all of them is noise on the wire and in the React render cycle.&lt;/p&gt;

&lt;p&gt;Two things to do. First, coalesce awareness updates on a fixed interval for agent peers — not per-operation:&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;let&lt;/span&gt; &lt;span class="nx"&gt;pendingAwarenessUpdate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;setTimeout&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&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;updateAgentAwareness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pos&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pendingAwarenessUpdate&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;pendingAwarenessUpdate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;awareness&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLocalStateField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cursor&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;anchor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;head&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;pendingAwarenessUpdate&lt;/span&gt; &lt;span class="o"&gt;=&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;span class="mi"&gt;300&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;Second, add a &lt;code&gt;type&lt;/code&gt; field to the agent's awareness state so the rendering layer can distinguish it from a human cursor without conditional logic scattered across components:&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;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;awareness&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLocalState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;agent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;streaming&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AI Assistant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;anchor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;insertionPoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;head&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;insertionPoint&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;"AI is writing" and "another person is typing" are different affordances. They deserve different visual treatments and different update rates. Encoding that distinction in the awareness state lets the rendering layer make the right call in one place.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Means for Your RFC
&lt;/h2&gt;

&lt;p&gt;The agent-as-peer pattern is the right architecture. Connecting the LLM to Yjs is not the hard part.&lt;/p&gt;

&lt;p&gt;The hard part is going back through every assumption your collaboration system makes about peer symmetry and making those assumptions &lt;em&gt;explicit&lt;/em&gt; — so you can break them deliberately for the agent peer without breaking them for everyone else.&lt;/p&gt;

&lt;p&gt;Concretely: your backpressure strategy assumed no single peer can dominate the convergence cycle, so it needs an application-layer token bucket scoped to the agent's origin. Your undo history assumed all tracked origins belong to the user, so the agent needs a separate &lt;code&gt;UndoManager&lt;/code&gt; surfaced as a distinct UI action. Your awareness rendering assumed cursor updates arrive at human speed, so agent presence needs coalescing and a type discriminant in the awareness state.&lt;/p&gt;

&lt;p&gt;None of these are hard to implement once you've named them. The risk is shipping the integration without naming them and finding the failure modes through user reports six weeks later when the collaborative load is real and the undo history is a mess.&lt;/p&gt;

&lt;p&gt;Treat rate limiting, undo isolation, and presence coalescing as first-class line items in the RFC. Not edge cases caught in code review.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The April 2026 demo and companion repo are at &lt;a href="https://electric.ax/blog/2026/04/08/ai-agents-as-crdt-peers-with-yjs" rel="noopener noreferrer"&gt;electric.ax/blog/2026/04/08/ai-agents-as-crdt-peers-with-yjs&lt;/a&gt;. The &lt;code&gt;y-prosemirror&lt;/code&gt; + awareness setup maps directly onto a TipTap stack — worth reading alongside the Yjs UndoManager docs if you're planning the integration.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Why this, why now:&lt;/em&gt; The April 2026 demo is the first working implementation of the agent-as-Yjs-peer pattern on a production-equivalent stack (&lt;code&gt;y-prosemirror&lt;/code&gt;, awareness protocol, Durable Streams), and it landed just weeks ago. The "agent velocity problem" it surfaces is genuinely new — CRDT literature has no prior answer for asymmetric peer throughput at this scale — and every team currently building collaborative AI editing features will hit the same three failure modes. Writing this now, before the pattern calcifies into bad defaults, is exactly the right time.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>frontend</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Global Precision &amp; Financial Calculations</title>
      <dc:creator>DimiDan</dc:creator>
      <pubDate>Sat, 30 May 2026 02:17:28 +0000</pubDate>
      <link>https://dev.to/dimidan/global-precision-financial-calculations-2bjf</link>
      <guid>https://dev.to/dimidan/global-precision-financial-calculations-2bjf</guid>
      <description>&lt;h2&gt;
  
  
  Global Precision and how it could impact the financial calculations
&lt;/h2&gt;

&lt;p&gt;When you're building anything that handles money, someone eventually asks: &lt;br&gt;
"how many decimal places should we use?" The tempting answer is to pick a &lt;br&gt;
number — 2, or maybe 5 to be safe — set it globally, and move on. It feels &lt;br&gt;
like a reasonable call. It's not.&lt;/p&gt;

&lt;p&gt;I've debugged precision bugs that traced back exactly to this decision. &lt;br&gt;
Once you understand why a single global precision fails, you can't unsee it.&lt;/p&gt;



&lt;p&gt;The core problem is that different parts of a financial system have genuinely &lt;br&gt;
different precision requirements, and treating them the same introduces errors &lt;br&gt;
at the boundaries.&lt;/p&gt;

&lt;p&gt;Billing calculations that appear on invoices need 2 decimal places. That's &lt;br&gt;
cent precision. That's what customers see, what they're charged, what appears &lt;br&gt;
on the contract. Showing 4 decimal places here is wrong for a different reason — &lt;br&gt;
you're creating a number that can't be represented in any real currency.&lt;/p&gt;

&lt;p&gt;Analytics calculations — revenue attribution, cohort aggregations, lifetime &lt;br&gt;
value — need more precision. When you're summing thousands of transactions &lt;br&gt;
before producing a final figure, rounding to 2 places at each step accumulates &lt;br&gt;
error that compounds in ways that matter at scale.&lt;/p&gt;

&lt;p&gt;Internal calculations — intermediate values mid-computation — should use the &lt;br&gt;
highest precision you can reasonably sustain, precisely because they feed into &lt;br&gt;
further operations. Rounding early and then doing arithmetic on the rounded &lt;br&gt;
value is one of the most common sources of financial calculation bugs I've &lt;br&gt;
encountered.&lt;/p&gt;

&lt;p&gt;If you apply a global setting, you're either truncating precision your &lt;br&gt;
analytics pipeline actually needs, or you're putting 4-decimal numbers on &lt;br&gt;
customer invoices. Neither is acceptable.&lt;/p&gt;



&lt;p&gt;The model that works is precision as an explicit input to each domain calculator, &lt;br&gt;
with sensible per-domain defaults:&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;PrecisionConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;internalCalcPlaces&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;displayPlaces&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;storagePlaces&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BILLING_PRECISION&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PrecisionConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;internalCalcPlaces&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;displayPlaces&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;storagePlaces&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The engine computes everything at internal precision. It rounds to display &lt;br&gt;
or storage precision only when producing output — at the domain boundary, &lt;br&gt;
not in the middle of a calculation chain.&lt;/p&gt;

&lt;p&gt;This brings us to what I think is the subtlest and most consequential mistake &lt;br&gt;
in financial arithmetic: rounding order.&lt;/p&gt;

&lt;p&gt;Consider a line item. Unit price $10.333..., quantity 3, 10% discount.&lt;/p&gt;

&lt;p&gt;Round early: you get $10.33 × 3 = $30.99, apply discount, round to &lt;strong&gt;$27.89&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Round late — maintain full precision through the chain: $10.333... × 3 × 0.9 = $27.899..., &lt;br&gt;
round only at output: &lt;strong&gt;$27.90&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;One cent difference on one line item. Across a large pricing table with currency &lt;br&gt;
conversion also in the chain, accumulated rounding error is real, customer-visible, &lt;br&gt;
and extremely difficult to debug once it's in production — because the bug is &lt;br&gt;
in the &lt;em&gt;order&lt;/em&gt; of operations, not in any individual calculation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Round at output boundaries. Never in the middle of a computation chain.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Currency conversion adds another layer to this. Rates are typically 4–6 decimal &lt;br&gt;
places. If you convert and immediately round to 2 decimal places before doing &lt;br&gt;
further arithmetic, you've thrown away precision you needed. Convert at full &lt;br&gt;
internal precision, display-round last.&lt;/p&gt;

&lt;p&gt;And never convert and re-convert. Round-trip currency conversion &lt;br&gt;
(&lt;code&gt;USD → EUR → USD&lt;/code&gt;) with intermediate rounding will not give you back the &lt;br&gt;
original number. If any part of your system displays a converted value and &lt;br&gt;
then uses that displayed value in further calculation, you have a latent &lt;br&gt;
bug waiting for the right combination of exchange rate and amount to surface it.&lt;/p&gt;




&lt;p&gt;The practical steps are straightforward: define precision per domain, make &lt;br&gt;
it an explicit parameter rather than global configuration, add a lint rule &lt;br&gt;
that forbids raw &lt;code&gt;Number&lt;/code&gt; arithmetic in your calculation package, and treat &lt;br&gt;
output boundaries as the one and only place where rounding is allowed.&lt;/p&gt;

&lt;p&gt;It sounds like ceremony until you've spent a day debugging a $0.01 discrepancy &lt;br&gt;
on a $50,000 contract that a customer has already signed and is asking why &lt;br&gt;
the numbers don't match.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Precision bugs that involve accumulated rounding are the hardest to reproduce &lt;br&gt;
consistently — they depend on exact input combinations and operation order. &lt;br&gt;
Has anyone built regression suites specifically for these? Property-based &lt;br&gt;
testing feels like the right tool but I'm curious what others have actually shipped.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>softwareengineering</category>
      <category>architecture</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Pricing logic feels boring until it's wrong.</title>
      <dc:creator>DimiDan</dc:creator>
      <pubDate>Thu, 21 May 2026 03:03:11 +0000</pubDate>
      <link>https://dev.to/dimidan/pricing-logic-feels-boring-until-its-wrong-4m4e</link>
      <guid>https://dev.to/dimidan/pricing-logic-feels-boring-until-its-wrong-4m4e</guid>
      <description>&lt;p&gt;&lt;strong&gt;Build it like infrastructure from day one.&lt;/strong&gt;&lt;br&gt;
Most pricing engines are built wrong. Here's what I'd do instead.&lt;br&gt;
If you're building a CPQ product — or any SaaS tool where users configure and price deals — your pricing logic is probably living in the wrong place.&lt;br&gt;
It's in a component. Or a utility function called from three different places. Or worse, duplicated between your frontend table renderer and your backend invoice service, silently drifting apart until a customer notices the numbers don't match.&lt;br&gt;
I've thought a lot about how to architect this properly. Here's what I'd do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First: treat pricing as infrastructure, not a feature&lt;/strong&gt;&lt;br&gt;
The moment you have billing frequency, line-item discounts, currency formatting, and tax rules composing together, you don't have a utility anymore. You have a domain. It deserves its own package, its own test suite, and its own ownership.&lt;br&gt;
A shared @your-org/pricing-engine package — published internally, consumed by your frontend, your backend, and your export pipeline — means one place where&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;unitPrice × quantity × frequency = subtotal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is defined. Not three.&lt;br&gt;
Why does this matter in practice? Consider this scenario:&lt;br&gt;
A sales rep quotes a client €90/month per seat for a SaaS tool, after a 10% discount, converted from USD. That number needs to be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Identical in the live proposal table the rep is editing&lt;/li&gt;
&lt;li&gt;Identical in the PDF the client downloads and signs&lt;/li&gt;
&lt;li&gt;Identical in the invoice the billing system generates on day one&lt;/li&gt;
&lt;li&gt;Identical in the revenue report the finance team pulls at month end&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your frontend, your PDF export service, and your billing backend each have their own implementation of applyDiscount() and convertCurrency(), you will eventually have a discrepancy. Not maybe. Eventually.&lt;/p&gt;

&lt;p&gt;Never use floating-point arithmetic for money. Ever.&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="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mf"&gt;0.30000000000000004&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isn't a JavaScript quirk. It's &lt;strong&gt;IEEE 754&lt;/strong&gt; — the way binary floating-point works at a hardware level. And it will silently corrupt your customer invoices.&lt;br&gt;
A real example of how this surfaces: a pricing table with 7 line items, each with a percentage discount and a currency conversion applied. By the time you sum those rows, the float drift compounds. Your table shows $1,200.00. Your invoice says $1,199.99. Your customer notices. Your support team gets a ticket. Your engineers spend a day debugging something that was never going to work correctly.&lt;br&gt;
Use &lt;em&gt;decimal.js&lt;/em&gt; or equivalent to it. Treat it as a hard rule, not a code style preference. Decimal arithmetic is slower — negligibly so at any realistic pricing table scale. There is no valid argument for floating-point in customer-facing money calculations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep your engine pure&lt;/strong&gt;&lt;br&gt;
A pricing engine should be a pure function:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;No Redux. No React. No HTTP calls. No side effects. Just data in, calculated data out.&lt;br&gt;
What the engine looks like from the outside&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computeTablePricing&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;rows&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;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;row-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;unitPrice&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;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;100.00&lt;/span&gt;&lt;span class="dl"&gt;"&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;billingFrequency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;monthly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;discount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;percentage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&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;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10&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="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EUR&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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;symbolPosition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;front&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;decimalPlaces&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;taxRate&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;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&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="c1"&gt;// result.rows["row-1"].subtotal === Decimal("450.00")&lt;/span&gt;
&lt;span class="c1"&gt;// result.frequencyTotals.monthly === Decimal("450.00")&lt;/span&gt;
&lt;span class="c1"&gt;// result.grandTotal === Decimal("450.00")&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters more than it sounds. A pure engine:&lt;/p&gt;

&lt;p&gt;Runs identically on web, server, React Native, and in a unit test&lt;br&gt;
Can be validated against your old implementation in shadow mode before touching production&lt;br&gt;
Can be reasoned about without understanding your component tree&lt;br&gt;
Can be tested with plain input/output assertions — no mocking, no rendering, no Redux store setup&lt;/p&gt;

&lt;p&gt;The billing frequency problem — a concrete example of why this matters&lt;br&gt;
Let's say you want to add billing frequency to your pricing table. Line items can be one-time, monthly, or annual. The footer should show a subtotal per frequency group.&lt;br&gt;
In a component-centric architecture, you add grouping logic to your table renderer. Then you realize your PDF export also needs frequency subtotals, so you add it there too. Then billing needs it. Three implementations. Three places to get out of sync.&lt;br&gt;
With an engine, billing frequency is just a first-class input field on each row:&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;billingFrequency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;one-time&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="s1"&gt;weekly&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="s1"&gt;monthly&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="s1"&gt;quarterly&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="s1"&gt;annual&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The engine computes frequency subtotals as outputs. The table reads them. The PDF reads them. The billing service reads them. Same numbers everywhere, because it's the same function.&lt;/p&gt;

&lt;p&gt;The discount composition problem — where things really break&lt;br&gt;
Discounts are where scattered pricing logic becomes a genuine product risk.&lt;br&gt;
Consider what a real CPQ discount model looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A rep can apply up to 10% at line-item level without approval
Anything above 10% needs manager sign-off
There's a volume discount: 5+ seats get an additional 5% off
There's a seasonal promotion: 15% off annual plans in Q4
These can stack — but only in specific combinations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If discount logic lives in your table component, how does your catalog apply the same rules? How does your billing service validate that the agreed discount is still applied at invoice time? How do you write a unit test for the approval threshold without rendering a table?&lt;br&gt;
&lt;strong&gt;The answer is:&lt;/strong&gt; you can't, cleanly. You end up with discount logic scattered across five files, each with slightly different behavior.&lt;br&gt;
In an engine architecture, discount application is a shared rule:&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;// shared/discount-application.ts&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;applyDiscount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;baseAmount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;discount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;percentage&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="s1"&gt;fixed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Decimal&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="nl"&gt;discountedAmount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;discountAmount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Decimal&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;discountAmount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;discount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;percentage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;baseAmount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;discount&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;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;discount&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="na"&gt;discountedAmount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;baseAmount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;discountAmount&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;discountAmount&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;One function. Tested once. Used by the table calculator, the catalog calculator, the billing service, and the approval workflow validator. Identical behavior everywhere by definition.&lt;/p&gt;

&lt;p&gt;The feature composition problem — this is the real ceiling&lt;br&gt;
Individual features are manageable. The problem is when they compose.&lt;br&gt;
A line item that has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Billing frequency: monthly
Discount: 10% off
Currency: EUR, converted from USD at current rate
Tax: 20% VAT applied after discount
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...needs to produce exactly the same number in your live editor, your PDF, your invoice, and your analytics pipeline. Every combination of features multiplies the number of cases where scattered logic can diverge.&lt;br&gt;
This is what actually blocks CPQ roadmaps. Not any individual feature — the combinatorial explosion of feature interactions when your calculation logic is spread across the codebase.&lt;/p&gt;

&lt;p&gt;Let your state layer stay clean&lt;br&gt;
In a Redux architecture, the pattern that actually works:&lt;/p&gt;

&lt;p&gt;Redux stores confirmed inputs only (what the user committed — prices, quantities, discounts, frequencies)&lt;br&gt;
Selectors derive all calculated values by passing those inputs through the engine&lt;br&gt;
Components never calculate — they only display&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;// selector reads inputs from Redux, derives outputs via engine&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selectTablePricingResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;selectTablePricingInputs&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputs&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;computeTablePricing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// pure function, memoized automatically&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This eliminates an entire class of bugs where your displayed subtotal disagrees with what gets saved, invoiced, or exported. The Redux store is always clean data. Derived values are never persisted. There is no ambiguity about whether a stored subtotal field is a user input or a computed value — a distinction that causes real bugs in collaborative editing and undo/redo flows.&lt;/p&gt;

&lt;p&gt;The migration play — how to get there without breaking production&lt;br&gt;
If you're refactoring an existing system rather than greenfielding, shadow mode is your best friend.&lt;br&gt;
Run the new engine in parallel with your old implementation. For every calculation your old code produces, the engine produces the same calculation independently. Log any divergence. Ship zero user-facing changes until the outputs match exactly — across every row type, every discount combination, every currency, every edge case you can throw at it.&lt;br&gt;
Silent arithmetic regressions on customer invoices are not a recoverable situation. Shadow mode gives you mathematical certainty before you flip the switch.&lt;br&gt;
The sequencing I'd recommend:&lt;/p&gt;

&lt;p&gt;Engine package first — pure functions, no UI changes, full test coverage&lt;br&gt;
New features through the engine — billing frequency, discounts, anything net-new goes through the engine from day one, minimizing regression risk on existing behavior&lt;br&gt;
Shadow mode on existing features — validate the engine matches current behavior exactly&lt;br&gt;
Migrate existing consumers — replace old calculations one surface at a time, behind a feature flag&lt;br&gt;
Remove the old code — only after full coverage and monitoring confirms correctness&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this unlocks long-term&lt;/strong&gt;&lt;br&gt;
Once you have a pure, shared pricing engine, a lot of things that were hard become straightforward:&lt;/p&gt;

&lt;p&gt;Backend adoption: Your invoice service imports the same npm package your frontend uses. Calculation discrepancies between frontend and backend become structurally impossible&lt;br&gt;
Mobile: React Native consumes the same engine. No separate mobile pricing logic&lt;br&gt;
Analytics: Revenue reports use the same calculation rules as the proposals that generated the revenue&lt;br&gt;
Formula engine: User-programmable pricing formulas&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scheme"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;=quantity&lt;/span&gt; &lt;span class="nv"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;unitPrice&lt;/span&gt; &lt;span class="nv"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;1&lt;/span&gt; &lt;span class="nv"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;discount&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;become an extension of the engine, not a rewrite of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The point&lt;/strong&gt;&lt;br&gt;
Pricing logic feels boring until it's wrong. Build it like infrastructure from day one — pure, shared, tested, and decoupled from your UI layer. The payoff isn't visible immediately. It's visible when your fifth pricing feature composes correctly with your first four without a single edge case meeting.&lt;br&gt;
Have you tackled pricing complexity at scale? Curious what patterns have worked — especially around currency and tax.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>typescript</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
