<?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>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 Staff Engineer's 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: What You Need to Know Before Building in 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, Three Assumptions Break</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>
