<?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: Maksym Kuzmitskyi (MaximusFT)</title>
    <description>The latest articles on DEV Community by Maksym Kuzmitskyi (MaximusFT) (@maximusft).</description>
    <link>https://dev.to/maximusft</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%2F1819104%2Fd158bf22-3ff5-498a-bab8-91ce4b684bc1.jpg</url>
      <title>DEV Community: Maksym Kuzmitskyi (MaximusFT)</title>
      <link>https://dev.to/maximusft</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/maximusft"/>
    <language>en</language>
    <item>
      <title>The Columns Hidden Inside Your Customer Reviews</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Thu, 17 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/the-columns-hidden-inside-your-customer-reviews-3oaf</link>
      <guid>https://dev.to/maximusft/the-columns-hidden-inside-your-customer-reviews-3oaf</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzujgg1tzi4sdufaydpyk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzujgg1tzi4sdufaydpyk.png" alt="The Columns Hidden Inside Your Customer Reviews" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A restaurant has ten thousand reviews. Its dashboard has an average rating, a monthly trend, and a search box.&lt;/p&gt;

&lt;p&gt;Somewhere inside those reviews is a much more useful answer: people like the food, but delivery packaging keeps ruining it.&lt;/p&gt;

&lt;p&gt;There is no column for that.&lt;/p&gt;

&lt;p&gt;That is what interests me about TypeSafe's Jev. I want to look at it as a way to turn human text into data: reviews, comments, forum discussions, the messy sentences that contain signals our databases cannot currently query.&lt;/p&gt;

&lt;p&gt;I recently wrote about a &lt;a href="https://ma-x.im/blog/silpo-ai-factory-horeca-procurement-agent" rel="noopener noreferrer"&gt;restaurant procurement agent&lt;/a&gt;. That project started with operational inputs: menus, portions, inventory, supplier availability. Here I want to look at another side of the same business. Customers are already describing problems. How do those descriptions become something an engineer can aggregate and a restaurant owner can investigate?&lt;/p&gt;

&lt;p&gt;This is an architecture I would explore, not a report from a deployed Jev integration. The examples below are hypothetical. I haven't run a benchmark on restaurant reviews.&lt;/p&gt;

&lt;h2&gt;
  
  
  A prep station for language
&lt;/h2&gt;

&lt;p&gt;Think about a restaurant kitchen before service. Ingredients arrive in different shapes. Someone washes, separates, portions, and puts them into containers the rest of the kitchen can work with.&lt;/p&gt;

&lt;p&gt;I see a similar job between raw text and analytics.&lt;/p&gt;

&lt;p&gt;A review arrives as a paragraph. The next stage produces a few useful attributes. Conventional software groups and counts them. A person, or a more capable language model with access to the evidence, can then work on the interpretation.&lt;/p&gt;

&lt;p&gt;The distinction matters because &lt;a href="https://typesafe.ai/blog/introducing-system-one-models-and-jev" rel="noopener noreferrer"&gt;Jev gives up free-form string generation&lt;/a&gt;. TypeSafe presents it as a model for fast, structured decisions. It doesn't browse Reddit for you or write the research report. You supply the material and the questions.&lt;/p&gt;

&lt;p&gt;Its &lt;a href="https://docs.typesafe.ai/primitives" rel="noopener noreferrer"&gt;three question types&lt;/a&gt; are Noul, a yes/no probability; Choice, a selection from supplied alternatives with probabilities; and Score, a position along defined levels. That is enough to add many useful attributes to an existing record. It doesn't make it an arbitrary extractor of new names, quotations, or product identifiers.&lt;/p&gt;

&lt;p&gt;If I need the exact restaurant name, I would preferably get it from source metadata. If I need an exact quotation, I would preserve the original text. Asking a classifier to choose between known categories is a different operation from asking a model to discover and return an unrestricted string.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The interesting AI feature is the column you couldn't populate before.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  One review can contain several different problems
&lt;/h2&gt;

&lt;p&gt;Consider this invented delivery review:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“The noodles were great, but the sauce leaked through the bag. This is the second time. I'd order again if they changed the containers.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A single positive/negative label throws away most of what makes that useful.&lt;/p&gt;

&lt;p&gt;I would ask whether the author praises the food, reports a packaging failure, describes a repeated problem, and makes a future order conditional on a change. Those signals can coexist. Forcing them into one winning category would lose information before the analysis even starts.&lt;/p&gt;

&lt;p&gt;At the application boundary, a normalized record might look like this. The values are illustrative, and this is my storage shape, not a Jev API response:&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;ReviewSignals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;reviewId&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;restaurantId&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;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delivery&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;direct&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;forum&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;publishedAt&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;foodPraiseProbability&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;packagingFailureProbability&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;repeatProblemProbability&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;conditionalReturnProbability&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;modelVersion&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;questionSetVersion&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 first four fields come from ingestion. The next four are model judgments. The last two let me explain how those judgments were produced.&lt;/p&gt;

&lt;p&gt;I would keep the raw review linked to this row. An owner clicking “packaging complaints increased” should be able to read the evidence, including the examples the classifier got wrong.&lt;/p&gt;

&lt;p&gt;Question wording deserves as much care as the schema. “Is packaging bad?” invites a broad interpretation. “Does the author report that the container or bag leaked, broke, or failed to contain the food?” defines an observable claim.&lt;/p&gt;

&lt;p&gt;Even then, a complaint describes the author's account. It doesn't establish which party caused the failure. A crushed meal might involve the container, the courier, or both. I would resist naming the field &lt;code&gt;restaurant_packaging_defect&lt;/code&gt; unless the evidence actually supports that attribution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parallel questions, then parallel records
&lt;/h2&gt;

&lt;p&gt;There are two kinds of parallel work here, and I would keep them separate in the design.&lt;/p&gt;

&lt;p&gt;For one review, ask all the independent questions together. TypeSafe documents that questions in a request share the same input state and are evaluated independently. One answer does not become context for the next. If a later question requires information fetched using an earlier answer, that needs another step. &lt;a href="https://docs.typesafe.ai/primitives" rel="noopener noreferrer"&gt;Question semantics&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Across the dataset, workers process different reviews concurrently. That is our ingestion and scheduling problem: queues, bounded concurrency, retries, rate limits, and resumable jobs. Parallel question evaluation doesn't promise unlimited corpus throughput.&lt;/p&gt;

&lt;p&gt;The pipeline I have in mind is quite ordinary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source exports / permitted APIs
        ↓
Normalize records, retain context, remove duplicates
        ↓
Queue → workers → Jev questions for each record
        ↓
Versioned semantic attributes + original record links
        ↓
SQL aggregates → evidence review → optional written report

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

&lt;/div&gt;



&lt;p&gt;I would give each result a key built from the source record, content hash, model version, and question-set version. Retrying a request should not create a second observation in the dashboard. Changing a question should not silently overwrite the previous measurement.&lt;/p&gt;

&lt;p&gt;The kitchen analogy has a useful limit here. You can prepare ingredients in parallel, but putting twice as many cooks in one doorway won't double dinner service. The slowest part might be collecting the data, resolving duplicates, or reviewing ambiguous examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let SQL do the counting
&lt;/h2&gt;

&lt;p&gt;Once those columns exist, familiar tools become useful again.&lt;/p&gt;

&lt;p&gt;Here is an illustrative PostgreSQL query over enriched delivery reviews. The 0.8 cutoff is a placeholder to validate against labeled examples, not a recommended universal threshold:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;restaurant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'month'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;published_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;count&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="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;analyzed_reviews&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;count&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="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;packaging_failure_probability&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;flagged_packaging_reviews&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;round&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="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;count&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="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;packaging_failure_probability&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;count&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="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;flagged_share_pct&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;review_signals&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'delivery'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;model_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;model_version&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;question_set_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;question_set_version&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;restaurant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'month'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;published_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;That last number is the share of analyzed reviews flagged by this rule. It is not the percentage of orders with failed packaging. We don't have all orders in this table. We have people who wrote reviews, within whatever collection process we used.&lt;/p&gt;

&lt;p&gt;I'd put that distinction in the dashboard label. Otherwise a technically correct query becomes a misleading product feature.&lt;/p&gt;

&lt;p&gt;The next useful question might be whether flagged reviews concentrate around one location, a delivery channel, or a period after a container change. Those joins require reliable operational metadata. Jev cannot supply missing order history by interpreting a paragraph more confidently.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same idea works for products
&lt;/h2&gt;

&lt;p&gt;Now replace the restaurant with a coffee grinder.&lt;/p&gt;

&lt;p&gt;“Loud” can mean several things. One owner mentions the noise and still recommends it. Another says it wakes their child and explains why they returned it. A third repeats something they heard without owning the product.&lt;/p&gt;

&lt;p&gt;I would want separate signals for claimed ownership, noise complaints, reported returns, and explicit reasons for returning. These are much more useful than a single sentiment score when deciding what to investigate about a product.&lt;/p&gt;

&lt;p&gt;The model still only judges what the text supports. “The author claims to own it” is a defensible label. “Verified purchaser” requires purchase evidence outside the text.&lt;/p&gt;

&lt;p&gt;There is also a discovery problem. A fixed question set finds the issues I thought to ask about. If a new failure mode appears and none of my questions cover it, a cheap classifier can miss it at enormous scale.&lt;/p&gt;

&lt;p&gt;I would regularly inspect a diverse sample of unflagged and uncertain records, and use open-ended analysis to propose new categories. Then I would label examples, evaluate the revised questions, and backfill comparable periods. Otherwise the dashboard can look stable simply because its vocabulary stopped evolving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reddit is a research corpus, with a boundary
&lt;/h2&gt;

&lt;p&gt;The Reddit version interests me even more: a deliberately scoped collection of posts and comments about a product or buying decision.&lt;/p&gt;

&lt;p&gt;Suppose I want to investigate why people discussing home espresso equipment regret a purchase. I would define the communities, time window, collection method, and inclusion criteria first. Then I could classify first-hand reports, price objections, maintenance complaints, and explicit alternatives people are considering.&lt;/p&gt;

&lt;p&gt;A comment like “same here” needs its parent. A sarcastic reply can reverse the apparent meaning. I would pass the relevant context with the comment and make clear which text is the target of the judgment. I would also retain thread relationships so that one lively discussion doesn't masquerade as fifty independent purchasing experiences.&lt;/p&gt;

&lt;p&gt;Collection needs its own implementation, using access appropriate to the source. Jev begins after we have the records; it doesn't solve that access problem.&lt;/p&gt;

&lt;p&gt;What could the output honestly say? Something like: “Within this collected set of discussions, maintenance complaints were more frequent among posts describing regret.”&lt;/p&gt;

&lt;p&gt;It could not establish the percentage of all espresso-machine owners who regret buying one. A million comments still reflect the people, communities, search terms, and periods that brought those comments into the dataset.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Scale makes a sample bigger. It doesn't automatically make it representative.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Honestly, this is where I think the engineering gets interesting. Processing the text cheaply is only one part. Defining what the resulting number means is the part the product has to get right.&lt;/p&gt;

&lt;h2&gt;
  
  
  The price changes what is worth trying
&lt;/h2&gt;

&lt;p&gt;As checked on September 17, 2026, TypeSafe's &lt;a href="https://docs.typesafe.ai/cookbooks/parallel_questions" rel="noopener noreferrer"&gt;parallel-questions cookbook&lt;/a&gt; uses a Jev input price of $0.042 per million tokens and zero output-token cost. That is a published pricing assumption, not my measured bill or a guarantee of future pricing.&lt;/p&gt;

&lt;p&gt;At that rate, a hypothetical million records averaging 1,000 total billed input tokens each would cost $42 for model input. “Total” matters: the budget must include the text, context, questions, and other billed request content. Collection, storage, retries, evaluation, and human review are separate costs.&lt;/p&gt;

&lt;p&gt;That makes a broad enrichment pass worth considering. I would still measure tokens and throughput on a realistic pilot before extrapolating.&lt;/p&gt;

&lt;p&gt;The same cookbook reports roughly 12.2× lower cost and 10× lower latency for batching thirteen questions over one document instead of asking them separately. That is a vendor example of batching, not evidence that my million-review pipeline will finish ten times faster.&lt;/p&gt;

&lt;p&gt;For me, the appealing consequence is practical: I can consider retaining several narrowly defined signals per record instead of asking one overloaded question and hoping its answer will support every future analysis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Correct types still need correct judgments
&lt;/h2&gt;

&lt;p&gt;TypeSafe's launch discussion distinguishes schema correctness from decision correctness. A model constrained to valid choices can still choose the wrong one. Its “zero hallucinations” language should be read in that narrower sense. &lt;a href="https://typesafe.ai/blog/introducing-system-one-models-and-jev" rel="noopener noreferrer"&gt;Launch explanation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I would start with a labeled evaluation set spanning clear examples, mixed opinions, sarcasm, short replies, and the languages actually present in the corpus. For each signal, I would check false positives and missed cases. A packaging-complaint filter and a claim about purchase intent may need different acceptance rules.&lt;/p&gt;

&lt;p&gt;A returned probability also deserves validation on that specific task. I wouldn't interpret every 0.9 as a demonstrated 90% correctness rate in my data. TypeSafe's &lt;a href="https://docs.typesafe.ai/confidence" rel="noopener noreferrer"&gt;confidence documentation&lt;/a&gt; explains the supplied measures; whether they support my operating threshold still needs evidence from the intended workload.&lt;/p&gt;

&lt;p&gt;Ambiguous cases can remain uncertain, go to human review, or receive a second analysis. If I exclude them from a chart, I want their count visible. Quietly dropping the hard cases can make a trend look cleaner than the underlying evidence.&lt;/p&gt;

&lt;p&gt;Finally, a writing model should receive computed aggregates, definitions, coverage limitations, and selected source excerpts. It can help explain the results. It should not invent the counts, turn a correlation into a cause, or claim that a few selected quotes prove a population-wide trend.&lt;/p&gt;

&lt;h2&gt;
  
  
  The useful thing is what becomes queryable
&lt;/h2&gt;

&lt;p&gt;I keep coming back to that restaurant with ten thousand reviews.&lt;/p&gt;

&lt;p&gt;The review text already contains more detail than the star rating can express. What I want is a way to preserve some of that detail in a form ordinary software can work with, while keeping the original words close enough to challenge the interpretation.&lt;/p&gt;

&lt;p&gt;That is the role I would give Jev: a focused processing stage between human language and the database. The report comes later. First I want to know which columns are worth creating, how reliably we can populate them, and what decisions they actually help someone make.&lt;/p&gt;

&lt;p&gt;If you have a pile of reviews or community discussions that your current dashboard can't explain, I'd be interested to hear which missing column you wish you could query.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>dataengineering</category>
      <category>productengineering</category>
    </item>
    <item>
      <title>Earned Autonomy: Comparing My Agent Workflow with a Corporate Experiment</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Wed, 16 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/earned-autonomy-comparing-my-agent-workflow-with-a-corporate-experiment-377c</link>
      <guid>https://dev.to/maximusft/earned-autonomy-comparing-my-agent-workflow-with-a-corporate-experiment-377c</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fagyro66kpfh4cfenn7ja.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fagyro66kpfh4cfenn7ja.png" alt="Earned Autonomy: Comparing My Agent Workflow with a Corporate Experiment" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I came to Liberty roughly six months ago with a fairly strong opinion about what an AI coding agent should be doing.&lt;/p&gt;

&lt;p&gt;Not autocomplete. Not a faster way to generate a function. Not a chat window that occasionally knows how to edit a file.&lt;/p&gt;

&lt;p&gt;I had been building a working environment in which an agent could participate in most of the delivery lifecycle: understand a task, inspect the code, make a plan, change a small slice, run the right checks, prepare a pull request, and help work through what happened afterwards.&lt;/p&gt;

&lt;p&gt;Now Liberty is starting to explore a more formal way of working with agents. The approach uses reusable agent skills and a more explicit, specification-driven sequence around planning, implementation, validation, and learning.&lt;/p&gt;

&lt;p&gt;I find the comparison more useful than either approach in isolation. I already have a workflow that works for me. The company experiment gives me a chance to ask a harder question: which parts of that workflow are genuinely valuable, and which parts only feel valuable because I built them myself?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The interesting question is not whether an agent can act autonomously. It is whether its autonomy has been earned by context, rules, and evidence.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What I brought with me
&lt;/h2&gt;

&lt;p&gt;My approach is based on earned autonomy.&lt;/p&gt;

&lt;p&gt;That phrase matters. I don't mean giving an agent unlimited freedom and hoping that a capable model will make good decisions. I mean giving it room to move inside boundaries that are explicit enough to be checked.&lt;/p&gt;

&lt;p&gt;In a typical task, the agent starts with a goal from a ticket or a conversation. It researches the relevant code and nearby context. It tries to find the code that actually controls the behavior instead of editing the first file that looks related. It identifies constraints, forms a local hypothesis about how the change should work, and chooses a cheap check that could prove the hypothesis wrong.&lt;/p&gt;

&lt;p&gt;Then it changes a small slice and validates that change immediately. Broader checks come afterwards. When the work is ready, the agent can prepare a branch and a draft pull request, help investigate CI failures, and respond to review feedback.&lt;/p&gt;

&lt;p&gt;The important part is not the length of that list. It is that the list describes a connected workflow rather than a collection of isolated prompts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context before cleverness
&lt;/h3&gt;

&lt;p&gt;The workflow depends on several levels of context. There are personal rules about how I want an agent to work, shared engineering standards, and repository-specific instructions. More local and more current information should win over a general preference.&lt;/p&gt;

&lt;p&gt;Memory helps the agent continue across sessions, but memory is not authority. The current code, tests, documentation, and actual tool output are more trustworthy than something saved from a previous conversation.&lt;/p&gt;

&lt;p&gt;That distinction is easy to state and surprisingly important in practice. A remembered convention can be stale. A test failure in front of you cannot be argued away by a paragraph written last month.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tools instead of context theatre
&lt;/h3&gt;

&lt;p&gt;I also prefer integrations over manually copying the world into a prompt. When permitted, the agent can work with task systems, documentation, source code, tests, and pull requests through connected tools.&lt;/p&gt;

&lt;p&gt;The benefit is not that the agent suddenly knows everything. It is that the relevant evidence can stay close to the task. Requirements, implementation, and validation do not have to be reconstructed from a chain of pasted fragments every time.&lt;/p&gt;

&lt;p&gt;That still requires judgment. An integration can expose the wrong context just as efficiently as the right one. Tools expand an agent's reach; they don't decide what deserves attention.&lt;/p&gt;

&lt;h3&gt;
  
  
  The human is still the owner
&lt;/h3&gt;

&lt;p&gt;An agent can research, edit, test, and prepare changes. It does not own the requirements, the architecture decision, the approval, or the final review.&lt;/p&gt;

&lt;p&gt;That is not a ceremonial disclaimer. It is part of the design. Autonomy is useful when it removes mechanical coordination and preserves human attention for decisions that carry real risk. If a workflow asks a person to approve every harmless step, it has not created autonomy. It has created a queue of tiny approvals.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Liberty is exploring
&lt;/h2&gt;

&lt;p&gt;The approach Liberty is beginning to investigate puts more structure around the work. It uses reusable agent skills: packages of instructions and working patterns that help an agent perform a particular kind of engineering task more consistently.&lt;/p&gt;

&lt;p&gt;A skill might support context discovery, planning, implementation, debugging, or review. It is not another intelligent employee. It is a reusable description of how to approach a task, with the aim of making useful practices easier to repeat across engineers and repositories.&lt;/p&gt;

&lt;p&gt;The broader lifecycle is more explicit than the one I normally follow informally:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Prepare the relevant instructions and context.&lt;/li&gt;
&lt;li&gt;Clarify or create a specification.&lt;/li&gt;
&lt;li&gt;Produce a plan.&lt;/li&gt;
&lt;li&gt;Review the plan.&lt;/li&gt;
&lt;li&gt;Implement the change.&lt;/li&gt;
&lt;li&gt;Validate the result.&lt;/li&gt;
&lt;li&gt;Record observations about quality and usability.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is a lot to like here. A team cannot rely on one engineer having spent months tuning a personal workflow. Reusable skills offer a possible common language. They might help engineers with different levels of experience start from a better baseline, and they create a clearer shape for comparing what happened across tasks.&lt;/p&gt;

&lt;p&gt;The experiment is still early. Liberty is exploring and piloting this kind of systematic approach; it has not produced a final company-wide process or a conclusion about fully autonomous software development. That distinction matters because the value of the experiment is precisely that the result is not known yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the approaches meet
&lt;/h2&gt;

&lt;p&gt;The overlap is larger than the difference.&lt;/p&gt;

&lt;p&gt;Both approaches assume that an agent needs good context before it needs more freedom. Both treat clear requirements, planning, small changes, tests, and human review as controls rather than optional polish. Both value reusable knowledge, because repeating the same explanation in every session is a poor use of anyone's time.&lt;/p&gt;

&lt;p&gt;Both also suggest that speed of generation is an incomplete measure. A change that appears quickly but needs extensive correction is not necessarily faster. The useful question includes quality, traceability, review effort, and what happens when the next engineer has to understand the result.&lt;/p&gt;

&lt;p&gt;That is why I don't see reusable skills as the opposite of my workflow. They may be a way to package some of its useful habits so that they are less dependent on personal configuration. They may also expose parts of my process that are too implicit to teach to anyone else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I am cautious
&lt;/h2&gt;

&lt;p&gt;The concerns I have are hypotheses, not verdicts. The experiment needs to test them.&lt;/p&gt;

&lt;p&gt;The first is process proportionality. A full specification, plan, review, implementation, and reporting sequence makes sense for risky or complex work. For a tiny, obvious change, it may create more text and more transitions than value. A good engineering process should scale with the risk of the task, not apply the same ceremony to everything.&lt;/p&gt;

&lt;p&gt;The second is that artifacts can become the goal. A detailed specification can still describe a wrong assumption. An agent can produce a polished plan for a problem nobody actually has. Evidence and useful questions matter more than document length.&lt;/p&gt;

&lt;p&gt;The third is the cost of formal stopping points. If a person must confirm every safe step, the workflow can become a stream of interruptions. My current model delegates many low-risk decisions through standing rules and brings a human in where the risk changes. I want to know whether a more formal process can preserve that shape.&lt;/p&gt;

&lt;p&gt;There is also a locality problem. A reusable skill can explain how to plan or validate in general, but it cannot automatically know a repository's architecture, conventions, environmental limits, or history of decisions. Without that local layer, a skill can produce work that sounds reasonable and still does not belong in the system.&lt;/p&gt;

&lt;p&gt;Finally, attribution will be difficult. If a task goes well, the cause might be the skill, the repository instructions, the connected tools, the tests, the quality of the original request, or the engineer's own experience. A fair experiment has to separate those effects as much as it reasonably can.&lt;/p&gt;

&lt;p&gt;Reporting has a cost too. Tracking corrections, defects, rework, and subjective experience is necessary for an honest comparison. But if reporting takes a meaningful amount of engineering time, that time is part of the result, not an inconvenient detail to hide.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I want to learn
&lt;/h2&gt;

&lt;p&gt;I am not joining this experiment to prove that my existing approach is already the answer. That would make the comparison pointless.&lt;/p&gt;

&lt;p&gt;I want to find out which skills prevent mistakes that would otherwise reach review. Where does an explicit specification improve the result, and where does it add ceremony? Does the process help an agent recover context in a new session? How much human correction is actually required? What happens to review quality and cost?&lt;/p&gt;

&lt;p&gt;The useful comparison will come from real tasks, not from a polished demonstration. For each task, I would want to understand whether the acceptance criteria were met, how much correction and rework appeared, which defects were found by the agent, CI, or a person, and which parts of the framework produced observable value.&lt;/p&gt;

&lt;p&gt;I would also separate ordinary engineering effort from framework overhead and reporting time. Those are different costs. Combining them into one number would make the result look cleaner while making it less informative.&lt;/p&gt;

&lt;p&gt;The data will need to be aggregated and anonymized. The goal is to learn about the workflow, not to publish internal details or turn a small experiment into a claim about the whole company.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is the beginning, not the conclusion
&lt;/h2&gt;

&lt;p&gt;There is a temptation to choose a winner early. Personal workflows feel concrete because you can see their history. Formal frameworks feel credible because they have names, stages, and reusable artifacts. Neither feeling is evidence.&lt;/p&gt;

&lt;p&gt;My current expectation is that the useful outcome will be a combination. Some skills may capture practices worth making more consistent. Some formal stages may be valuable only for certain classes of work. Some of my existing rules may turn out to be personal preferences rather than durable engineering principles.&lt;/p&gt;

&lt;p&gt;That is a good result. The point is not to protect a workflow because I built it, or to accept a framework because it arrived with a process diagram. The point is to discover which controls help an agent do better work while leaving people responsible for the decisions that matter.&lt;/p&gt;

&lt;p&gt;I am at the beginning of that comparison now. Later, once there is enough evidence and the results have been checked for accuracy and confidentiality, I'll come back with what actually happened: where reusable skills helped, where they added friction, and what I changed in my own way of working.&lt;/p&gt;

&lt;p&gt;Until then, I am keeping the conclusion deliberately open. An agent does not become trustworthy because it has more freedom, more documents, or more steps. It becomes more trustworthy when the system around it makes good decisions easier to verify.&lt;/p&gt;

&lt;p&gt;If you've been comparing a personal agent workflow with a more formal team process, I'd be interested in what you measured. The difference between a convincing demo and a useful engineering system usually appears only after the first few ordinary tasks.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>architecture</category>
      <category>engineeringworkflow</category>
    </item>
    <item>
      <title>The Cart Was Not the Product: Building a HoReCa Procurement Agent for Silpo AI Factory</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Tue, 15 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/the-cart-was-not-the-product-building-a-horeca-procurement-agent-for-silpo-ai-factory-5e3m</link>
      <guid>https://dev.to/maximusft/the-cart-was-not-the-product-building-a-horeca-procurement-agent-for-silpo-ai-factory-5e3m</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhw9wcfoxbsocq5x29341.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhw9wcfoxbsocq5x29341.png" alt="The Cart Was Not the Product: Building a HoReCa Procurement Agent for Silpo AI Factory" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The obvious idea was also the least interesting one.&lt;/p&gt;

&lt;p&gt;The Silpo AI Factory hackathon gave participants an unusually tempting setup: AI, an official MCP server from a large grocery retailer, product search, carts, delivery, purchase history, and the requirement to build an agent that could actually use tools.&lt;/p&gt;

&lt;p&gt;The easy version almost writes itself. A user asks what to buy. AI suggests products. The app puts them into a cart.&lt;/p&gt;

&lt;p&gt;Useful? Maybe.&lt;/p&gt;

&lt;p&gt;Interesting? Not enough.&lt;/p&gt;

&lt;p&gt;That shape quickly becomes another AI shopping chat. I wanted to avoid that, partly because the hackathon itself was clearly nudging people toward something more agentic: not text generation around commerce, but a system where the agent receives context, uses tools, and helps complete a concrete task.&lt;/p&gt;

&lt;p&gt;The project that came out of that became a HoReCa Procurement Agent: an AI-assisted procurement cockpit for a small restaurant that also handles catering and events.&lt;/p&gt;

&lt;p&gt;Links for the project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hackathon: &lt;a href="https://ai-factory.silpo.ua/" rel="noopener noreferrer"&gt;Silpo AI Factory&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Official Silpo MCP: &lt;a href="https://ai-factory.silpo.ua/docs/mcp" rel="noopener noreferrer"&gt;ai-factory.silpo.ua/docs/mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Live demo: &lt;a href="https://horeca-nine-alpha.vercel.app/" rel="noopener noreferrer"&gt;horeca-nine-alpha.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source code: &lt;a href="https://github.com/MaximusFT/horeca" rel="noopener noreferrer"&gt;github.com/MaximusFT/horeca&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Demo video: &lt;a href="https://youtu.be/5D2QP2OFflM" rel="noopener noreferrer"&gt;YouTube&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The shopping assistant was too small
&lt;/h2&gt;

&lt;p&gt;The first direction was some version of Smart Basket, AI cart, shopping assistant. It is a reasonable idea. It is also the first idea most people would reach for.&lt;/p&gt;

&lt;p&gt;That was the problem.&lt;/p&gt;

&lt;p&gt;I kept asking one question: if I remove the AI chat, what remains as a product?&lt;/p&gt;

&lt;p&gt;For a generic shopping assistant, the answer was not strong enough. The AI was doing too much of the conceptual work. The product was mostly a conversation with a cart mutation at the end.&lt;/p&gt;

&lt;p&gt;So we changed the buyer.&lt;/p&gt;

&lt;p&gt;What if the customer is not one person deciding what to eat, but a small restaurant or catering business trying to plan procurement?&lt;/p&gt;

&lt;p&gt;That changed everything. A grocery cart stopped being a list of preferences. It became the result of an operational problem: regular restaurant demand, bookings, events, guest counts, menus, recipes, inventory, incoming supply, safety stock, shelf life, and supplier availability.&lt;/p&gt;

&lt;p&gt;Now the official commerce MCP had a more precise role. It did not define the business need. It helped execute a need the system had already calculated.&lt;/p&gt;

&lt;p&gt;That boundary became the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Catering was close, but not enough
&lt;/h2&gt;

&lt;p&gt;The first stronger version was catering procurement.&lt;/p&gt;

&lt;p&gt;Wedding. 180 guests. Menu. Recipes. Ingredients. Shopping list. Supplier cart.&lt;/p&gt;

&lt;p&gt;That already felt much better than a shopping assistant. A single change, like 180 guests becoming 200, can flow through portions, recipe quantities, ingredient demand, packages, and cart preparation.&lt;/p&gt;

&lt;p&gt;Good demo.&lt;/p&gt;

&lt;p&gt;Still not quite the product.&lt;/p&gt;

&lt;p&gt;The flaw was simple: a restaurant continues to exist between events. It still needs vegetables, meat, dairy, eggs, coffee, bread, and all the ordinary supplies of daily operations. Event demand is only one source of demand.&lt;/p&gt;

&lt;p&gt;After that correction, the model became much stronger:&lt;/p&gt;

&lt;p&gt;Regular restaurant demand + event and catering demand + safety stock - current inventory - incoming supply = procurement need.&lt;/p&gt;

&lt;p&gt;Then procurement need becomes: when to buy, which supplier products can satisfy it, what package sizes are available, what is in stock, and what should be prepared for approval.&lt;/p&gt;

&lt;p&gt;That is the point where the idea stopped being an event shopping calculator and became a procurement layer for HoReCa.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The cart is not the product. The procurement plan is the product.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The cart is the final execution step. The product is the plan that explains what the business needs, when it needs it, and why.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI should not do the arithmetic
&lt;/h2&gt;

&lt;p&gt;Once the product model became clearer, another decision followed naturally: most of the important work was not AI work.&lt;/p&gt;

&lt;p&gt;Take the hero scenario. A wedding changes from 180 to 200 guests. The menu includes salmon croissants, cheese croissants, ham croissants, skewers, salads, and dessert cups. If each guest gets 0.35 salmon croissant and each croissant uses a known amount of salmon, that is not a prompt. That is arithmetic.&lt;/p&gt;

&lt;p&gt;The same is true for BOM expansion, unit conversion, inventory subtraction, FEFO, safety stock, package rounding, procurement dates, and plan diffing.&lt;/p&gt;

&lt;p&gt;If an LLM calculates those numbers, the system becomes less reliable, not more intelligent.&lt;/p&gt;

&lt;p&gt;So the procurement engine became ordinary TypeScript code. It owns the critical math: restaurant demand, event demand, recipes, ingredients, inventory lots, incoming supply, shortages, purchase dates, packages, and the procurement plan.&lt;/p&gt;

&lt;p&gt;The AI got a different job: understand natural-language intent, choose application tools, explain where a number came from, help match internal ingredients to supplier products, evaluate replacements, and orchestrate a sequence of safe tool calls.&lt;/p&gt;

&lt;p&gt;AI does not calculate procurement. AI helps operate the procurement system.&lt;/p&gt;

&lt;p&gt;That was probably the most important architecture decision in the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dashboard first, agent second
&lt;/h2&gt;

&lt;p&gt;I also did not want the user to open a procurement system in the morning and ask an AI, "so, what is happening today?"&lt;/p&gt;

&lt;p&gt;The application should already know.&lt;/p&gt;

&lt;p&gt;It should know upcoming events, restaurant load, procurement batches, shortages, expiry risks, incoming supply, and the next actions that need attention. The agent can help explain and act, but the primary interface should be an operations dashboard, not a blank chat box.&lt;/p&gt;

&lt;p&gt;So the UI settled into four main sections: Overview, Procurement, Events, and Inventory.&lt;/p&gt;

&lt;p&gt;The agent did not become a fifth top-level screen. It became a contextual drawer: ask why a quantity is needed, explain the impact of the wedding change, find a replacement, prepare the next supplier order.&lt;/p&gt;

&lt;p&gt;That distinction matters. A chat-first system makes the user discover the product by asking questions. A cockpit should surface the current operating state before the user asks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Misto Kitchen made the model concrete
&lt;/h2&gt;

&lt;p&gt;To avoid building against abstractions, we created a synthetic restaurant: Misto Kitchen.&lt;/p&gt;

&lt;p&gt;One kitchen. Shared inventory. Around 55 seats. Regular restaurant operations. Catering and events. A 14-day planning horizon in the Europe/Kyiv business timezone.&lt;/p&gt;

&lt;p&gt;The demo events gave the system real pressure: Birthday Breakfast, Office Lunch, Private Anniversary, Tech Conference, and Wedding. The wedding became the hero flow: 180 guests to 200 guests, total event guests moving from 445 to 465.&lt;/p&gt;

&lt;p&gt;That tiny number change was useful because anyone can understand it. A manager gets a call: the wedding will have 200 people, not 180. In a manual process, that means recalculating menu portions, ingredients, inventory coverage, packaging, and supplier orders.&lt;/p&gt;

&lt;p&gt;In this system, the change creates a preview first. The user inspects the impact, then approves it. Only after approval does it become Plan v2.&lt;/p&gt;

&lt;p&gt;This is where the product became more than a planning spreadsheet. The system could explain what changed and why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Explainability had to be part of the model
&lt;/h2&gt;

&lt;p&gt;One number is not enough.&lt;/p&gt;

&lt;p&gt;If the system says "buy 31.4 kg of chicken," a real procurement manager has an immediate question: why 31.4?&lt;/p&gt;

&lt;p&gt;So provenance became part of the domain model. Each procurement line needed to explain its sources: restaurant operations, specific events, current inventory, incoming supply, safety stock, shelf-life rationale, and supplier enrichment state.&lt;/p&gt;

&lt;p&gt;The AI can summarize that explanation in human language. But it should not invent it. The source of truth is the deterministic calculation and structured provenance.&lt;/p&gt;

&lt;p&gt;This is one of those product decisions that looks like UX until you build it. Then it becomes architecture. If explanation is generated after the fact by the model, you get storytelling. If explanation is part of the calculation, you get auditability.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first UI was too engine-shaped
&lt;/h2&gt;

&lt;p&gt;After the deterministic core started working, the first noticeable product flaw was not in the math. It was in the Overview.&lt;/p&gt;

&lt;p&gt;The screen was too close to the engine. It wanted to show procurement batches, dated ingredient lines, restaurant operating days, and engine status. Those facts were correct, but they did not explain the product to a restaurant manager.&lt;/p&gt;

&lt;p&gt;So the Overview had to be rebuilt around the business story:&lt;/p&gt;

&lt;p&gt;Restaurant Operations + Events &amp;amp; Catering -&amp;gt; Combined Business Demand -&amp;gt; Procurement Plan -&amp;gt; Supplier Execution.&lt;/p&gt;

&lt;p&gt;The first viewport needed to show regular operations, the five events, upcoming deliveries, and attention items. Not because those cards are prettier. Because the user has to understand in 10-15 seconds that the system combines daily restaurant demand and event demand into one procurement plan.&lt;/p&gt;

&lt;p&gt;That was a useful reminder: a correct engine does not automatically create a clear product.&lt;/p&gt;

&lt;p&gt;There was also a very normal frontend bug. The Ask Misto drawer first looked trapped inside a narrow header. The issue was not the drawer size. A sticky header used &lt;code&gt;backdrop-filter&lt;/code&gt;, which changed the containing block for &lt;code&gt;position: fixed&lt;/code&gt; descendants. The fix was to render the fullscreen overlay through a React portal into &lt;code&gt;document.body&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Small bug. Good lesson. A feature can be logically correct and still be mounted in the wrong DOM and CSS context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mock supplier first, live MCP later
&lt;/h2&gt;

&lt;p&gt;Before live Silpo access was reliable, we built the full supplier flow against a mock supplier.&lt;/p&gt;

&lt;p&gt;That was not a fake replacement for integration. It was how we protected the product model from waiting on external access.&lt;/p&gt;

&lt;p&gt;The mock supplier covered product mappings for all 38 ingredients, package sizes, prices, availability, deterministic package rounding, surplus and cost, substitution approval, cart preview, cart write, reread, reconciliation, and preservation of unrelated cart lines. The preferred salmon SKU was intentionally unavailable, with a compatible 400 g alternative, so the product could exercise replacement handling.&lt;/p&gt;

&lt;p&gt;The architecture was simple on purpose:&lt;/p&gt;

&lt;p&gt;Procurement engine -&amp;gt; SupplierGateway -&amp;gt; Mock supplier or Silpo supplier.&lt;/p&gt;

&lt;p&gt;The mock stayed as an offline fallback. It should never be described as live Silpo.&lt;/p&gt;

&lt;p&gt;When the agent arrived, it was also intentionally small. One Procurement Agent. Not Inventory Agent, Forecast Agent, Supplier Agent, Cart Agent, and Supervisor Agent. For this MVP, that would have been architecture theatre.&lt;/p&gt;

&lt;p&gt;The agent worked through application-level tools like &lt;code&gt;get_event&lt;/code&gt;, &lt;code&gt;get_procurement_plan&lt;/code&gt;, &lt;code&gt;explain_requirement&lt;/code&gt;, &lt;code&gt;preview_event_change&lt;/code&gt;, &lt;code&gt;apply_event_change&lt;/code&gt;, and &lt;code&gt;prepare_supplier_order&lt;/code&gt;. It did not see the whole raw MCP catalog. It did not own procurement quantities. Every tool called an application use case with its own schema and approval rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Live AI was not the proof
&lt;/h2&gt;

&lt;p&gt;The default agent mode became &lt;code&gt;AGENT_MODE=local&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That sounds less exciting than a live model, but it was the right default for this project. It was deterministic, free, offline, used the same protected application tools, and made the demo stable enough to record.&lt;/p&gt;

&lt;p&gt;The OpenAI Responses API path was implemented as an explicit opt-in with a limited number of consecutive tool steps. But the final demo should not be described as a fully autonomous live OpenAI session. The API key was not configured in this workspace, and the protocol was covered with mocked HTTP tests.&lt;/p&gt;

&lt;p&gt;What the demo proves is still valuable: the agentic application flow, the tool orchestration, the approval boundaries, and the supplier execution path. But it is important to say what was actually tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  The official MCP changed the shape of the work
&lt;/h2&gt;

&lt;p&gt;The early generic JSON-RPC client and public docs were enough to understand the direction. They were not enough for a reliable live integration.&lt;/p&gt;

&lt;p&gt;After access, the real path required OAuth 2.1: Dynamic Client Registration, authorization code, PKCE S256, refresh token, a session-scoped provider, the official &lt;code&gt;@modelcontextprotocol/sdk&lt;/code&gt;, Streamable HTTP transport, and server-side OAuth state.&lt;/p&gt;

&lt;p&gt;The rule at that stage was simple: do not invent Silpo arguments, fields, or output shapes.&lt;/p&gt;

&lt;p&gt;First, we captured &lt;code&gt;tools/list&lt;/code&gt; through an authorized session on September 1, 2026. It returned 40 unique tools, not the 39 suggested by the earlier public description. One additional branch was &lt;code&gt;silpo_create_shopping_cart&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Input draft-07 schemas were saved and validated with Ajv before MCP calls. Output schemas were not treated as known in advance. The mapper only read documented paths and returned safe diagnostics when a shape did not match, without leaking values.&lt;/p&gt;

&lt;p&gt;That is where integration work starts to become real. Not at the first successful tool call. At OAuth, schemas, expiry, idempotency, reread, and observability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read first, then write
&lt;/h2&gt;

&lt;p&gt;The first live workflow was deliberately read-only:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;silpo_get_my_shopping_cart&lt;/code&gt; -&amp;gt; &lt;code&gt;silpo_get_shopping_cart_by_id&lt;/code&gt; -&amp;gt; &lt;code&gt;silpo_get_time_slots&lt;/code&gt; -&amp;gt; &lt;code&gt;silpo_find_products_batch&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Search ran for eggs, tomatoes, and salmon. The workflow stopped before writes if there was no cart, not enough cart context, an invalid delivery slot, or a response shape that did not match documented paths. Express delivery normalized to &lt;code&gt;DeliveryHome&lt;/code&gt;, because that is what the product search tool description required.&lt;/p&gt;

&lt;p&gt;Only after that came a constrained write spike:&lt;/p&gt;

&lt;p&gt;preview -&amp;gt; explicit human approval -&amp;gt; one additive write -&amp;gt; immediate cart reread -&amp;gt; validation.&lt;/p&gt;

&lt;p&gt;Existing cart lines could not be cleared or replaced automatically. On September 2, 2026, production confirmed one limited additive write for a test candidate with quail eggs. The sanitized trace showed &lt;code&gt;silpo_add_or_update_cart_products&lt;/code&gt; followed by &lt;code&gt;silpo_get_shopping_cart_by_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The UI reported a verified result only after reread.&lt;/p&gt;

&lt;p&gt;That is not automatic checkout. It is not payment. It is a controlled cart mutation with evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Serverless made state honest
&lt;/h2&gt;

&lt;p&gt;The wedding change flow revealed another boundary.&lt;/p&gt;

&lt;p&gt;Locally, an in-memory repository was convenient. In production, a memory singleton is not a storage strategy. Different Vercel workers can handle different requests, so planning state and event previews had to move behind a real repository boundary.&lt;/p&gt;

&lt;p&gt;The final approach allowed local development to use in-memory state, while deployed planning state and event previews used Turso. Apply and reread went through the repository boundary, and tests covered preview, apply, and refresh through different repository instances.&lt;/p&gt;

&lt;p&gt;That bug did not invalidate the product model. It made the deployment model more honest.&lt;/p&gt;

&lt;p&gt;The corporate-machine constraints also shaped the workflow. Local coding, tests, and static checks happened on the work machine. Silpo OAuth and OTP happened in a personal browser. Deployed backend calls handled Silpo MCP. GitHub Actions covered sanitized trace and Turso smoke checks. Secrets and raw cart/product values were not routed through the model or stored in traces.&lt;/p&gt;

&lt;p&gt;Not glamorous. Necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Live integration made the mock better
&lt;/h2&gt;

&lt;p&gt;The supplier-neutral gateway eventually connected the same application contract to the official MCP:&lt;/p&gt;

&lt;p&gt;UI -&amp;gt; SupplierOrderService -&amp;gt; SupplierGateway -&amp;gt; SilpoSupplierGateway -&amp;gt; official MCP.&lt;/p&gt;

&lt;p&gt;The first live rollout was bounded. It did not try to send the whole batch. It selected up to three light, fully fulfillable lines within a ten-line window, with total mass or volume capped at 20 kg/l, far below the observed 50 kg Silpo delivery validation. The cap was checked before preview and before write.&lt;/p&gt;

&lt;p&gt;This was a pilot, not a claim that the entire procurement batch could safely go to Silpo.&lt;/p&gt;

&lt;p&gt;The gateway accounted for display ratio, step, price, stock, package rounding, product IDs already in the cart, error-level cart validations, and reread after write. After live verification, matching became stock-aware and started skipping SKUs that could not fully satisfy the need.&lt;/p&gt;

&lt;p&gt;The salmon replacement case was especially useful. In the mock catalog, the preferred salmon SKU was unavailable, so the offline flow showed a substitution. In live Silpo, &lt;code&gt;silpo_get_replacements&lt;/code&gt; for sampled salmon returned success with an empty list.&lt;/p&gt;

&lt;p&gt;That empty list was not an error. It meant there was no known picking or assembler risk for that product. The system had to accept reality instead of inventing a replacement for a prettier demo. Unresolved lines blocked cart preview. Unknown nested shapes did not become synthetic replacements. Exact nested replacement mapping would only be added after a separate capture.&lt;/p&gt;

&lt;p&gt;The integration became more reliable because it allowed the real response to be boring.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failures after live verification were the useful ones
&lt;/h2&gt;

&lt;p&gt;Several issues appeared only after the live path existed.&lt;/p&gt;

&lt;p&gt;A delivery slot could expire between preparation and apply, so the system needed recovery: reread cart, fetch available slots, preview alternatives, require approval, update the slot, reread the exact slot, and retry supplier preparation.&lt;/p&gt;

&lt;p&gt;A repeated demo run could accidentally double the real cart quantity. Reset Demo must not delete real Silpo cart lines, so the gateway now reads existing product IDs before supplier initialization and stops before preview/write if a selected SKU is already present.&lt;/p&gt;

&lt;p&gt;Insufficient stock also changed matching. A known product is not enough if there is not enough stock to fulfill the required quantity.&lt;/p&gt;

&lt;p&gt;Later, i18n exposed a runtime boundary: a Dictionary with interpolation functions cannot be passed from a Server Component to a Client Component as a plain prop. Typecheck and build passed, but the browser failed. The fix was to pass a plain locale to client components and let them read a client-safe dictionary import. Backend locale also flows into agent explanations and supplier flow.&lt;/p&gt;

&lt;p&gt;These are not glamorous failures. They are the difference between a demo that works once and a workflow that can be trusted enough to show.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability had to be safe
&lt;/h2&gt;

&lt;p&gt;A normal browser cannot directly show a server-to-server call from Vercel backend to &lt;code&gt;mcp.silpo.ua&lt;/code&gt;. It only sees requests to the Next.js routes.&lt;/p&gt;

&lt;p&gt;So the demo needed a sanitized Agent request execution timeline.&lt;/p&gt;

&lt;p&gt;It combines application decisions, application tool calls, official Silpo MCP calls, sequence number, status, and duration. In mock mode, the timeline shows only application steps. In deployed Silpo mode, it adds blue Silpo MCP entries from the server-side trace.&lt;/p&gt;

&lt;p&gt;That mattered because a reviewer needs to see where the application made a decision and where the official supplier tool was actually called. But the trace stays safe: operation names, argument key names, status, duration, and structural summary. Not tokens. Not raw arguments. Not addresses. Not cart contents. Not product values.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the final demo actually proves
&lt;/h2&gt;

&lt;p&gt;The final demo flow was intentionally short.&lt;/p&gt;

&lt;p&gt;Overview shows Restaurant Operations, Events &amp;amp; Catering, Combined Procurement, a 14-day timeline, and attention items. Wedding starts at 180 guests. The user changes it to 200, reviews the preview, sees Plan v1 become Plan v2, and approves. The system updates total guests to 465 and records the change.&lt;/p&gt;

&lt;p&gt;Then the user opens Why this quantity? and sees deterministic provenance: demand sources, inventory coverage, incoming supply, and timing. Finally, Ask Misto prepares the next supplier order, shows a bounded supplier card, handles availability/substitution state, creates a cart preview, requires approval, performs an additive write, rereads the cart, and reports a verified result.&lt;/p&gt;

&lt;p&gt;The right ending is not "AI ordered everything."&lt;/p&gt;

&lt;p&gt;The right ending is: the agent guided the user from a business event change, through an explainable procurement plan, to a verified supplier cart, while keeping business-critical mutations under human control.&lt;/p&gt;

&lt;p&gt;Testing followed the same philosophy. Unit and application tests covered demand calculation, BOM expansion, FEFO, incoming supply timing, package rounding, provenance, wedding preview/apply, stale preview protection, agent tool schemas, mock supplier flow, reread reconciliation, live schema validation, timeslot updates, sanitized MCP trace, and Turso repositories.&lt;/p&gt;

&lt;p&gt;The handoff baseline had 122 tests in 36 files. Static checks included &lt;code&gt;npm test&lt;/code&gt;, &lt;code&gt;npm run typecheck&lt;/code&gt;, &lt;code&gt;npm run lint&lt;/code&gt;, &lt;code&gt;npm run build&lt;/code&gt;, and &lt;code&gt;npm audit&lt;/code&gt;, with audit reporting 0 vulnerabilities after adding the MCP SDK.&lt;/p&gt;

&lt;p&gt;Browser QA covered the wedding flow, explanation drawer, mock salmon substitution, cart preview -&amp;gt; approval -&amp;gt; reread, agent supplier preparation, expired slot recovery, mobile/tablet navigation at 850 px and 390 px, no page-level horizontal overflow, procurement search, expiry-risk filtering, and localized units.&lt;/p&gt;

&lt;p&gt;Production Silpo verification confirmed live OAuth, 40 tools from &lt;code&gt;tools/list&lt;/code&gt;, cart context, approved timeslot update, batch product search, one explicitly approved additive product write, immediate reread and validation, a bounded supplier flow through the normal Procurement Agent UI, one &lt;code&gt;silpo_add_or_update_cart_products&lt;/code&gt; call followed by &lt;code&gt;silpo_get_shopping_cart_by_id&lt;/code&gt;, and preservation of existing cart lines.&lt;/p&gt;

&lt;p&gt;It did not prove everything. It did not test all 40 tools, full checkout, payment, full wholesale ordering, every replacement response shape, live OpenAI autonomy, or real restaurant usage.&lt;/p&gt;

&lt;p&gt;That distinction is the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would keep
&lt;/h2&gt;

&lt;p&gt;The hardest part of this project was not connecting an LLM to a store.&lt;/p&gt;

&lt;p&gt;The hardest part was deciding what work the AI should not do.&lt;/p&gt;

&lt;p&gt;The procurement engine calculates. The plan explains. The UI surfaces the operating state. The agent interprets intent and orchestrates tools. The supplier gateway executes bounded supplier operations. MCP is the final execution layer, not the product itself.&lt;/p&gt;

&lt;p&gt;That boundary made the project more interesting than another shopping assistant.&lt;/p&gt;

&lt;p&gt;Silpo is a connected commerce supplier in this story. I am not presenting it as a full HoReCa wholesale ERP, and the MVP is not a multi-supplier procurement platform. The future architecture could absolutely grow that way: Silpo, Metro, meat suppliers, vegetable suppliers, local suppliers, with AI helping compare availability, delivery, price, quality constraints, and preferences.&lt;/p&gt;

&lt;p&gt;But that was not the MVP.&lt;/p&gt;

&lt;p&gt;The MVP was narrower and, honestly, stronger because of it: one restaurant, one kitchen, one procurement plan, one agent, one supplier integration, explicit approvals, idempotent writes, reread validation, and a trace that shows what happened without leaking sensitive values.&lt;/p&gt;

&lt;p&gt;If there is one thing I would take from the hackathon, it is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Real agentic software is not proven by a model calling a tool. It is proven by the system around that tool: deterministic logic, explainable state, preview, approval, idempotency, reread, durable storage, and a clear boundary between application and supplier.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is where the work became interesting. Not when the cart changed. When the cart became only the last step of a procurement workflow the system could explain.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>architecture</category>
      <category>hackathon</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Micro-Frontends Are an Org Chart Decision, Not a Technical One</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Mon, 14 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/micro-frontends-are-an-org-chart-decision-not-a-technical-one-5cj2</link>
      <guid>https://dev.to/maximusft/micro-frontends-are-an-org-chart-decision-not-a-technical-one-5cj2</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fakoyb2r5snozzokisokg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fakoyb2r5snozzokisokg.png" alt="Micro-Frontends Are an Org Chart Decision, Not a Technical One" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've sat through more than one micro-frontends pitch, and they all open the same way: a slide about independent deployments, a slide about teams not blocking each other, maybe a slide about being able to use a different framework per app "if we ever need to." It's a technical pitch, delivered with technical slides, aimed at a technical audience.&lt;/p&gt;

&lt;p&gt;Here's the thing I think gets buried under all of that: almost none of it is actually a technical problem. It's an organizational one wearing a technical costume, and the pitch works better when it's honest about that from the start.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You don't split a frontend into micro-frontends because the code is too big. You split it because the &lt;em&gt;teams&lt;/em&gt; touching that code can no longer coordinate at the speed the org needs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's not a knock on micro-frontends. It's a claim about what actually justifies them — and it means the right first question isn't "is our bundle too big" or "do we need framework flexibility." It's: does our org chart already have the shape that micro-frontends are supposed to reflect?&lt;/p&gt;

&lt;h2&gt;
  
  
  What the technical benefits actually cost
&lt;/h2&gt;

&lt;p&gt;Every micro-frontend benefit on the slide has a real, un-glamorous cost that doesn't make it into the pitch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Independent deploys&lt;/strong&gt; sound great until you notice that a single-repo monolith with a decent CI pipeline can &lt;em&gt;also&lt;/em&gt; deploy independently deployable features behind flags, without ever splitting the app. What micro-frontends actually buy you over that is independent deploys &lt;em&gt;without needing anyone else's review or coordination&lt;/em&gt; — which is a genuine win, but only if the reason you needed that isolation was that coordinating with the other team was the actual bottleneck, not the deploy pipeline itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Framework isolation&lt;/strong&gt; — "team B can use Vue while team A uses React" — sounds like flexibility and is usually a cost nobody priced in. You now ship two frameworks' runtimes to every user, you've doubled your hiring and knowledge-sharing surface, and the actual reason teams reach for this is almost never "Vue is better for this specific problem." It's normally "team B inherited a Vue app during an acquisition and nobody wants to rewrite it." That's a real reason. It's an organizational one, not an argument that framework diversity is good architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smaller bundles per team&lt;/strong&gt; is the one that sounds most like a real technical win, and it's the one I'd push back on hardest. Splitting an app into micro-frontends doesn't shrink the total code the user downloads — it usually grows it, because now you're shipping duplicated dependencies (React itself, your design system, your date library) once per micro-frontend unless you invest heavily in shared-dependency tooling, which is its own significant ongoing cost. If bundle size is your actual problem, code-splitting inside a single app solves it directly, cheaper, and without any of what follows in this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost nobody puts on the slide
&lt;/h2&gt;

&lt;p&gt;Once you've actually split the frontend, a list of problems shows up that no diagram warned you about.&lt;/p&gt;

&lt;p&gt;Cross-app state gets hard in a way that's easy to underestimate. A shopping cart, a logged-in user, a feature flag — anything that needs to be consistent across micro-frontends now needs a real synchronization mechanism, and "we'll just use custom events" is the sentence every team says right before building an ad hoc, under-tested message bus by accident.&lt;/p&gt;

&lt;p&gt;Design system drift becomes a constant, low-grade tax. Each micro-frontend can update its dependencies, including your shared UI kit, on its own schedule — which is the whole point — and that means you will, eventually, ship a page where the button in the header and the button in the body are visually two versions apart, because two teams updated on two different weeks.&lt;/p&gt;

&lt;p&gt;And orchestration — routing between micro-frontends, sharing auth state, handling the moment one micro-frontend fails to load while the rest of the page is fine — is genuinely hard engineering that a single-app architecture simply doesn't need to solve, because there's only one app.&lt;/p&gt;

&lt;p&gt;None of these costs are disqualifying. They're the price of the thing micro-frontends are actually good at: letting teams ship independently without waiting on each other. The mistake is paying that price for a benefit you didn't need.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question that actually decides it
&lt;/h2&gt;

&lt;p&gt;So here's the test I'd apply before anyone reaches for micro-frontends: &lt;strong&gt;can two teams currently ship features to production without one blocking the other, in the architecture you already have?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the honest answer is yes — one repo, decent ownership boundaries, feature flags, a CI pipeline that doesn't make Team A wait on Team B's review — then you don't have the problem micro-frontends solve. You have a problem that looks similar (big app, multiple teams) but isn't actually causing the pain micro-frontends are priced to fix, and adopting them will hand you the costs above in exchange for nothing.&lt;/p&gt;

&lt;p&gt;If the honest answer is no — teams are routinely blocked on each other's release windows, a shared repo's CI queue has become a real bottleneck, or (the case I'd take most seriously) you've inherited genuinely separate applications from an acquisition or a long history of independent teams and merging them isn't realistic — that's the org chart telling you the org is already split. Micro-frontends, in that case, aren't creating the split. They're the architecture that finally matches a split that already exists in how the company works.&lt;/p&gt;

&lt;p&gt;That's Conway's Law, and I don't think it's a fun aside here — it's the actual decision criterion. Your system's structure is going to mirror your communication structure whether you plan for it or not. Micro-frontends are what you get when you deliberately let that happen instead of fighting it with an org chart your codebase doesn't reflect.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd actually check first
&lt;/h2&gt;

&lt;p&gt;Before signing off on a micro-frontends architecture, I'd want to see the org chart, not the bundle analyzer. How many teams actually own frontend code that needs to ship independently? Is the current blocker genuinely architectural, or is it a CI pipeline that could be fixed for a fraction of the cost of a rewrite? And if the teams merged into one tomorrow, would the case for micro-frontends survive that?&lt;/p&gt;

&lt;p&gt;If it wouldn't — if the whole justification evaporates the moment you imagine one team owning all of it — then you weren't looking at an architecture problem. You were looking at a coordination problem, and there's usually a cheaper fix for that than teaching your frontend to run as five separate applications that now have to talk to each other.&lt;/p&gt;

</description>
      <category>react</category>
      <category>architecture</category>
      <category>microfrontends</category>
      <category>reactplaybook</category>
    </item>
    <item>
      <title>Layered vs Feature-Sliced: Two Ways to Cut the Same App</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Sat, 12 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/layered-vs-feature-sliced-two-ways-to-cut-the-same-app-3b7b</link>
      <guid>https://dev.to/maximusft/layered-vs-feature-sliced-two-ways-to-cut-the-same-app-3b7b</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F29wg6aamwyu6wzctdi6c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F29wg6aamwyu6wzctdi6c.png" alt="Layered vs Feature-Sliced: Two Ways to Cut the Same App" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Take any non-trivial React app and you can describe its whole folder structure as the answer to one question: when you cut this codebase into pieces, which axis do you cut along? There are really only two honest answers, and almost every architecture discussion I've ever sat in is secretly an argument about which one a team picked without realizing they'd picked one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layered&lt;/strong&gt; cuts by technical kind. All the components live together, all the hooks live together, all the API calls live together, regardless of which feature they belong to.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
  components/
  hooks/
  api/
  utils/
  pages/

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Feature-sliced&lt;/strong&gt; cuts by product concern. Everything a feature needs — its components, its hooks, its API calls — lives together, and it's the &lt;em&gt;kind&lt;/em&gt; of file that becomes secondary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
  features/
    payment-method/
    order-history/
    account-settings/

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

&lt;/div&gt;



&lt;p&gt;Neither of these is a mistake. They're both legitimate answers to "how do I organize this," and the reason this keeps being a live debate is that each one is &lt;em&gt;correct&lt;/em&gt; for a while and then becomes &lt;em&gt;expensive&lt;/em&gt; in a completely different, entirely predictable way.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every architecture is a bet about which kind of future change you're optimizing for. Layered bets on stability of kind. Feature-sliced bets on stability of feature boundaries. Neither bet is safe forever.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What layered actually gives you
&lt;/h2&gt;

&lt;p&gt;Layered architecture is the one nobody has to be taught. A junior developer can guess where a new hook goes — &lt;code&gt;hooks/&lt;/code&gt; — without reading a single doc. That's not a small thing. It's the lowest-friction structure there is for the first several months of a project, precisely because it doesn't ask anyone to make a judgment call about feature boundaries before those boundaries are even clear yet.&lt;/p&gt;

&lt;p&gt;It's also genuinely good for a certain kind of change: anything that's cross-cutting by nature. If you're auditing every place the app calls the API layer, or refactoring every hook to a new data-fetching pattern, layered structure puts all of that in one folder, and you can see the whole blast radius by opening one directory.&lt;/p&gt;

&lt;p&gt;Where it breaks is exactly where products stop being simple. Once a feature — say, an order-management flow — starts touching a dozen components, several hooks, a couple of API modules, and its own utils, "layered" stops meaning "organized" and starts meaning "this feature's logic is now distributed across five folders that also contain twenty other features' logic." Want to delete the order-management feature? You're now grepping across the whole codebase, hoping you find every file that only exists because of it. Layered structure doesn't track feature boundaries, so nothing in the folder tree tells you where a feature ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  What feature-sliced actually gives you
&lt;/h2&gt;

&lt;p&gt;Feature-sliced structure solves exactly that problem. A feature's folder is a feature's blast radius. Deleting &lt;code&gt;features/order-history/&lt;/code&gt; deletes the feature — not perfectly, there's usually some shared code left behind, but close. Onboarding someone onto &lt;em&gt;one&lt;/em&gt; feature means pointing at &lt;em&gt;one&lt;/em&gt; folder, not explaining the layer system and hoping they can mentally reconstruct which fragments across five layers belong to the thing they're working on.&lt;/p&gt;

&lt;p&gt;It scales with team size in a way layered structure doesn't. Multiple teams, each owning a handful of features, can work with almost no folder-level collision, because they're rarely in each other's slices. That's the real argument for feature-sliced at scale — not "it's cleaner," but "it lets N teams work without stepping on each other's files."&lt;/p&gt;

&lt;p&gt;The cost shows up the moment two features need the same thing. Every feature-sliced codebase eventually accumulates a &lt;code&gt;shared/&lt;/code&gt; folder, and that folder has exactly the failure mode you'd expect: things get promoted to it reactively, usually the first time a second feature needs something, rarely with much thought about whether it's actually a stable, general-purpose piece or just two features that happen to look similar today. Left unmanaged, &lt;code&gt;shared/&lt;/code&gt; becomes the layered architecture's &lt;code&gt;utils/&lt;/code&gt; folder wearing a nicer name — a dumping ground, just one level removed.&lt;/p&gt;

&lt;p&gt;And there's a subtler cost: feature-sliced structure asks you to correctly guess feature boundaries &lt;em&gt;before&lt;/em&gt; the product has told you what they are. Early in a project, "is this one feature or two" is often genuinely unknowable, and picking wrong means a slice that either merges awkwardly with another later or splits painfully in two.&lt;/p&gt;

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

&lt;p&gt;I don't think there's a universal right answer here, and I'm suspicious of anyone who gives you one without asking what you're building. The question that actually matters is: &lt;strong&gt;what changes more often in this product — the technical kind of thing, or the feature it belongs to?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're building something where features are genuinely stable and rarely added (an internal admin tool with five screens that haven't changed in two years), layered structure's simplicity is worth more than feature-sliced's isolation, because you're not paying feature-sliced's up-front cost of guessing boundaries for a product that isn't going to grow new ones.&lt;/p&gt;

&lt;p&gt;If you're building something where the &lt;em&gt;number&lt;/em&gt; of features is the thing that grows — a product adding new user-facing capabilities every quarter, with multiple teams shipping in parallel — feature-sliced earns its cost almost immediately, because the layered alternative means every new feature adds friction to five existing folders instead of creating one clean new one.&lt;/p&gt;

&lt;p&gt;Most real products start in the first category and migrate into the second without anyone deciding to. That's the actual argument for feature-sliced conventions like FSD: not that they're objectively better, but that &lt;a href="https://ma-x.im/blog/react-playbook-what-fsd-actually-fixed" rel="noopener noreferrer"&gt;the underlying problem they solve&lt;/a&gt; — coordination at scale — is the one almost every successful product eventually has, even if it didn't start with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A hybrid is not cheating
&lt;/h2&gt;

&lt;p&gt;The reasonable middle ground, and the one I actually reach for on real projects, is layered at the top for genuinely global concerns — routing, design tokens, a handful of app-wide providers — and feature-sliced underneath for everything that's actually product surface area.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
  app/ // routing, providers, global config — layered, rarely touched
  shared/ // genuinely cross-feature primitives, promoted deliberately
  features/
    payment-method/ // feature-sliced from here down
    order-history/
    account-settings/

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

&lt;/div&gt;



&lt;p&gt;The discipline that makes this work isn't the folder names. It's being honest about which things are actually global (rare, and worth naming explicitly) versus which things just look similar between two features today and might diverge next quarter. Promote to &lt;code&gt;shared/&lt;/code&gt; when a third feature needs something, not when a second one does — that one rule prevents most of the premature abstraction that turns &lt;code&gt;shared/&lt;/code&gt; into a second &lt;code&gt;utils/&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;Pick the axis that matches what actually changes in your product, be honest about which folder is doing that job right now, and don't confuse "we chose a structure" with "we solved organization." You didn't solve it. You picked which future problem you'd rather have.&lt;/p&gt;

</description>
      <category>react</category>
      <category>architecture</category>
      <category>fsd</category>
      <category>reactplaybook</category>
    </item>
    <item>
      <title>Nivra: Turning a Hackathon Idea into a WebMCP Architecture Workspace</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Fri, 11 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/nivra-turning-a-hackathon-idea-into-a-webmcp-architecture-workspace-3dgl</link>
      <guid>https://dev.to/maximusft/nivra-turning-a-hackathon-idea-into-a-webmcp-architecture-workspace-3dgl</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmri5wjjsoaktqvlqxoql.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmri5wjjsoaktqvlqxoql.png" alt="Nivra: Turning a Hackathon Idea into a WebMCP Architecture Workspace" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hackathons usually start with a dangerous question: what can I build quickly?&lt;/p&gt;

&lt;p&gt;I don't like that question very much. It sounds practical, but it pushes you toward the wrong kind of demo: a screen, a button, a little AI, and the feeling that something product-shaped exists. For a normal hackathon, maybe that is enough. For the OpenAI WebMCP Challenge, it wasn't.&lt;/p&gt;

&lt;p&gt;The better question was narrower:&lt;/p&gt;

&lt;p&gt;What idea actually shows why WebMCP should exist?&lt;/p&gt;

&lt;p&gt;Not "how do I add AI to an app." Not "how do I put a chat panel next to a UI." I mean: where does an agent need to read the state of the application, move through the same workspace as the human, act on the same model, and help expose something that would otherwise stay hidden between layers of the system?&lt;/p&gt;

&lt;p&gt;That question became Nivra.&lt;/p&gt;

&lt;p&gt;Links for the project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live app: &lt;a href="https://nivra-psi.vercel.app" rel="noopener noreferrer"&gt;nivra-psi.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub repository: &lt;a href="https://github.com/MaximusFT/nivra" rel="noopener noreferrer"&gt;github.com/MaximusFT/nivra&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Demo video: &lt;a href="https://youtu.be/BSz_Aqm4Ius" rel="noopener noreferrer"&gt;YouTube&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The first ideas were not architecture
&lt;/h2&gt;

&lt;p&gt;At the beginning, I did not have the project. I had several possible directions, and we rejected most of them for reasons that were more useful than the ideas themselves.&lt;/p&gt;

&lt;p&gt;One strong candidate was Senwia Sleep Lab, an evolution of an app idea I had around sleep analysis. I still like that direction. Sleep is understandable. The data can be visual. An AI agent could explore patterns with the user, highlight anomalies, ask questions, and help connect habits with outcomes.&lt;/p&gt;

&lt;p&gt;But for this challenge, it had a weak point. To make the demo convincing, I would have needed a synthetic data layer: fake sleep history, fake events, fake metrics. WebMCP would have interacted with data I had invented for the demo. The product might have been interesting, but WebMCP would not have been the reason it worked.&lt;/p&gt;

&lt;p&gt;There were other options too. Interview Forge, an AI tool for technical interviews. AI Newsroom, a system for processing and organizing news. And then an architecture workspace: a place where a human architect and an AI agent could work together over a structured model of a software system.&lt;/p&gt;

&lt;p&gt;That last idea kept surviving the cuts.&lt;/p&gt;

&lt;p&gt;Not because architecture sounds more serious. Because WebMCP was not decorative there. If an agent is working with architecture, it needs to inspect application state, move between views, find dependencies, show evidence, add findings, create proposals, and work inside the same workspace the architect is looking at.&lt;/p&gt;

&lt;p&gt;That is a different product shape. WebMCP stops being a chat next to a diagram. It becomes the agent's interface into the architecture model.&lt;/p&gt;

&lt;h2&gt;
  
  
  ArchPilot was the wrong name, usefully
&lt;/h2&gt;

&lt;p&gt;The first working name was ArchPilot.&lt;/p&gt;

&lt;p&gt;It sounded fine at first. Short, obvious, close enough to architecture and agent-assisted work. Then we checked the market and found existing products with similar names, including products in software architecture and architecture governance.&lt;/p&gt;

&lt;p&gt;So the name had to go.&lt;/p&gt;

&lt;p&gt;That could have been just a naming problem. It turned into a product decision.&lt;/p&gt;

&lt;p&gt;When a name stops working, you have to ask what you were actually trying to name. And that forced a clarification: Nivra should not be "AI designs architecture for you." It should not be an architecture validator that stands above the system and declares what is correct. It should not be a governance platform. And it definitely should not be a chat box glued to the side of a diagram.&lt;/p&gt;

&lt;p&gt;I needed a different sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Architecture is the shared context.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That became the center of Nivra.&lt;/p&gt;

&lt;p&gt;The human and the AI agent work with the same structured architecture model, but through different interfaces. The human sees a visual workspace. The agent gets access to the same model through WebMCP tools. One source of truth. Two ways to reason over it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The demo needed a real architectural tension
&lt;/h2&gt;

&lt;p&gt;Once the product shape was clear, the next problem was the scenario.&lt;/p&gt;

&lt;p&gt;I did not want a toy example where service A talks to service B and the whole point is that the arrow is bad. That kind of demo teaches the user to agree with the author, not to inspect the system.&lt;/p&gt;

&lt;p&gt;The scenario became Commerce Platform.&lt;/p&gt;

&lt;p&gt;At the High-Level Design level, everything looks almost right. Product, Cart, Checkout, and Account are separated into microfrontends. Checkout looks independent. You can draw a box around it and say, quite reasonably, "this is a separate part of the system."&lt;/p&gt;

&lt;p&gt;Then the business requirement lands: the Checkout Team needs to evolve and deploy Checkout independently from Product.&lt;/p&gt;

&lt;p&gt;The natural reaction is understandable:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Checkout is already a separate microfrontend. Why wouldn't it be independently deployable?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is exactly where Nivra needed to become useful.&lt;/p&gt;

&lt;p&gt;At HLD, Checkout looks ready. But when the agent moves deeper into Checkout LLD, two dependencies appear, and they are not the same kind of problem.&lt;/p&gt;

&lt;p&gt;The first is &lt;code&gt;Basket Adapter -&amp;gt; Product Store&lt;/code&gt; through shared runtime state. That is runtime coupling. Checkout may be visually separate, but it still depends on Product state at runtime.&lt;/p&gt;

&lt;p&gt;The second is &lt;code&gt;Pricing Module -&amp;gt; Product Service&lt;/code&gt; through an explicit REST API. That is also a dependency, but it is visible, named, and easier to reason about. An architect may decide it is acceptable for now.&lt;/p&gt;

&lt;p&gt;That distinction became the core of the demo:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A separate box on a diagram doesn't mean an independent system.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I did not want Nivra to say "all dependencies are bad." That is not architecture. The useful moment is when the human sees the evidence, understands the difference between shared runtime state and an explicit contract, and makes a trade-off.&lt;/p&gt;

&lt;p&gt;The agent does not decide for the architect. It helps expose the layer where the decision actually lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  The canvas could not be the source of truth
&lt;/h2&gt;

&lt;p&gt;After the scenario clicked, we designed the product architecture around it.&lt;/p&gt;

&lt;p&gt;The most important decision was boring on purpose: do not start with the diagram.&lt;/p&gt;

&lt;p&gt;If the first thing you build is a React Flow canvas, it is very easy to confuse the graph with the architecture. The nodes on the screen start to feel like the system. Coordinates acquire meaning. Views drift into their own copies of elements. Then validation, WebMCP, and UI each interpret the "architecture" slightly differently.&lt;/p&gt;

&lt;p&gt;So the first real artifact was a plain TypeScript Architecture Model. Commerce Platform v1.35 became a deterministic fixture model, not a decorative picture. HLD and Checkout LLD referenced the same entities and relations. Views stored IDs, not duplicated elements. Layout lived separately, because canvas coordinates are presentation metadata, not architectural meaning.&lt;/p&gt;

&lt;p&gt;That decision paid for itself later. UI, validation, and WebMCP could all work against the same model independently. If the canvas changed, the architecture still existed. If the visual language changed, the domain model did not move with the pixels.&lt;/p&gt;

&lt;h2&gt;
  
  
  The plan came before the code
&lt;/h2&gt;

&lt;p&gt;After the product idea settled, we still did not jump straight into implementation.&lt;/p&gt;

&lt;p&gt;First came the Product Definition and MVP scope. Then Architecture Levels and the Domain Model. Then the WebMCP Tool Contract, UI/UX, technical implementation notes, demo fixture, golden scenario, seven-day implementation plan, and visual design specification.&lt;/p&gt;

&lt;p&gt;All of that became Nivra Master Specification v1.1.&lt;/p&gt;

&lt;p&gt;At that moment, Nivra still was not an application. But it was no longer just an idea. It had a scenario, a domain model, a WebMCP contract, a UI direction, a demo fixture, and a development plan.&lt;/p&gt;

&lt;p&gt;The question changed.&lt;/p&gt;

&lt;p&gt;It was no longer: what should we build?&lt;/p&gt;

&lt;p&gt;It became: can we make this model actually work?&lt;/p&gt;

&lt;h2&gt;
  
  
  Foundation first, WebMCP later
&lt;/h2&gt;

&lt;p&gt;Once the master specification existed, the temptation was obvious: start with tools. This was the WebMCP Challenge, after all. Show WebMCP.&lt;/p&gt;

&lt;p&gt;But that would have been the wrong order.&lt;/p&gt;

&lt;p&gt;We started with React, TypeScript, and Vite, but the real output of the first phase was not a screen. It was the Architecture Model and the operations around it.&lt;/p&gt;

&lt;p&gt;Only then came the canvas, adapters from the Architecture Model into React Flow, and navigation from Commerce HLD into Checkout LLD.&lt;/p&gt;

&lt;p&gt;That became Checkpoint A: does the product create a "now I see it" moment without the agent telling the user what to think?&lt;/p&gt;

&lt;p&gt;At HLD, Checkout looked separate. In LLD, the shared runtime state dependency and the explicit REST call appeared together. The relations had different visual and semantic treatments. The product idea finally stood on the workspace itself, not on narration around the demo.&lt;/p&gt;

&lt;p&gt;If a demo only works while I am explaining it out loud, the product does not work yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first serious bug was not where I expected
&lt;/h2&gt;

&lt;p&gt;The early version compiled. Then a browser smoke test immediately found a React maximum update depth problem, effectively an infinite render.&lt;/p&gt;

&lt;p&gt;The cause was not the Architecture Model. It was an object-returning Zustand selector. The selector created a new object, React treated it as a new value, the component updated, the selector created another new object, and the loop continued.&lt;/p&gt;

&lt;p&gt;The fix was ordinary: stable comparison with &lt;code&gt;useShallow&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The lesson was larger than the fix. Even a foundation screen cannot be considered successful just because TypeScript is satisfied. The browser has its own failure modes: selectors, subscriptions, effects, render timing. If an architecture workspace is supposed to be trustworthy, it needs to be exercised in a live UI early.&lt;/p&gt;

&lt;p&gt;Around the same time, another risk became obvious: the canonical fixture could not be mutated directly. If future write actions could silently change Current Architecture, the whole proposal story would lose credibility. Changes had to be immutable. Current Architecture had to remain the reference point.&lt;/p&gt;

&lt;p&gt;That sounds like an implementation detail, but it is really a product detail. The user has to know what they are looking at: the current system, or a proposed alternative.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reasoning loop had to work manually first
&lt;/h2&gt;

&lt;p&gt;Before registering any WebMCP tools, we made the manual scenario work end to end.&lt;/p&gt;

&lt;p&gt;The architect selects evidence, opens the Context Panel, sees relations and deployment information, creates a Finding, saves architectural constraints, runs deterministic validation, creates a Proposal, compares Current and Proposal, and validates the alternative.&lt;/p&gt;

&lt;p&gt;That was Checkpoint B: the full manual Observe -&amp;gt; Question -&amp;gt; Inspect -&amp;gt; Understand -&amp;gt; Decide -&amp;gt; Propose -&amp;gt; Verify loop.&lt;/p&gt;

&lt;p&gt;This is where the product became clearer to me: validation was not the main product.&lt;/p&gt;

&lt;p&gt;Validation matters. Without it, the whole thing becomes a nice story. But Nivra is not centered on pass/fail. It is centered on shared understanding. The agent can help inspect, explain, drill into LLD, and propose changes. But once a rule is formalized, boring deterministic code should check it.&lt;/p&gt;

&lt;p&gt;Agent-assisted reasoning. Deterministic verification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Constraints needed evidence
&lt;/h2&gt;

&lt;p&gt;We added four rule types: &lt;code&gt;forbidden-dependency&lt;/code&gt;, &lt;code&gt;independent-deployment&lt;/code&gt;, &lt;code&gt;no-cycles&lt;/code&gt;, and &lt;code&gt;allowed-protocol&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Validation was pure, typed, and evidence-bearing. Each check returned not only pass/fail, but references to the elements or relations that explained the result.&lt;/p&gt;

&lt;p&gt;Golden Current Architecture returned 2 passed / 2 failed.&lt;/p&gt;

&lt;p&gt;Honestly, that was better than making the demo pass perfectly from the beginning. If Current passes every rule, validation proves very little. It might just be confirming a happy path written for the demo. The 2/2 result showed that the rule was actually finding the hidden violation.&lt;/p&gt;

&lt;p&gt;Some details mattered. A forbidden dependency has to account for descendants, not only exact IDs. LLD elements inherit deployment unit through the parent hierarchy. Protocol comparison is case-insensitive, even if the display model says REST. Validation does not create Findings, and it does not depend on React, Zustand, or the canvas.&lt;/p&gt;

&lt;p&gt;An architecture rule should validate architecture, not whatever the UI happens to be rendering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proposal, not mutation
&lt;/h2&gt;

&lt;p&gt;The next problem was trust.&lt;/p&gt;

&lt;p&gt;If an agent can "fix" the architecture directly, the human loses the ability to understand the original state. What was Current? What did the agent propose? What has been accepted? What is still just an alternative?&lt;/p&gt;

&lt;p&gt;So Current Architecture stayed immutable. A Proposal became a patch-based alternative with base version checking.&lt;/p&gt;

&lt;p&gt;In the golden proposal, the runtime state dependency is removed and replaced with a Checkout Snapshot Contract. Not as a magical fix, but as a checkable architectural alternative.&lt;/p&gt;

&lt;p&gt;The visual diff needed thought too. The added Snapshot Contract and the removed Product Store/runtime relation had to be visible at the same time. The effective Proposal view had to be computed from the patch, not stored as a separate diagram, because otherwise we would be back to two competing models instead of one source of truth.&lt;/p&gt;

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

&lt;p&gt;Current: 2 passed / 2 failed.&lt;/p&gt;

&lt;p&gt;Proposal: 4 passed / 0 failed.&lt;/p&gt;

&lt;p&gt;That felt like architecture work. The human can see the trade-off, inspect the evidence, and evaluate a verified alternative before accepting anything.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The agent doesn't draw architecture for you. It reasons inside your architecture with you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  WebMCP as an adapter boundary
&lt;/h2&gt;

&lt;p&gt;Only after manual Checkpoint B did we start the WebMCP integration.&lt;/p&gt;

&lt;p&gt;That order mattered. WebMCP tools did not get their own architecture model. They did not mutate React Flow directly. They became a browser adapter over existing workspace operations. The human interface and the agent interface called the same logic.&lt;/p&gt;

&lt;p&gt;The first group of tools covered reading and navigation: &lt;code&gt;get_architecture&lt;/code&gt;, &lt;code&gt;inspect_element&lt;/code&gt;, and &lt;code&gt;show_architecture_view&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then came write and compute tools: &lt;code&gt;annotate_architecture&lt;/code&gt;, &lt;code&gt;add_constraint&lt;/code&gt;, &lt;code&gt;create_proposal&lt;/code&gt;, and &lt;code&gt;validate_architecture&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Seven P0 tools in total.&lt;/p&gt;

&lt;p&gt;On paper this sounds simple: write a JSON Schema, register a handler, done. In practice, the boundary was more interesting. The runtime can call a handler more than once. It can pass malformed data. It can send an ID that looks plausible but breaks the model. So the write tools also validate stable kebab-case IDs, enums, evidence references, duplicate and conflicting IDs, parent references, relation endpoints, proposal base version, and nested update values.&lt;/p&gt;

&lt;p&gt;Every call creates an activity entry with running, success, or error status. The human can see what the agent actually did in the workspace. But activity is not persisted as architecture state.&lt;/p&gt;

&lt;p&gt;That separation matters. Agent activity explains the process. It is not the architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experimental API means experimental
&lt;/h2&gt;

&lt;p&gt;WebMCP was an experimental browser API available through a specific Chromium mode. A normal browser might not have &lt;code&gt;document.modelContext&lt;/code&gt;. That could not be treated as the whole app failing.&lt;/p&gt;

&lt;p&gt;So Nivra got an explicit WebMCP ready / WebMCP unavailable status, a guarded legacy fallback for earlier preview runtimes, AbortSignal-based registration, a complete manual fallback without WebMCP, and a guided demo simulation for cases where an external agent is not available.&lt;/p&gt;

&lt;p&gt;The honesty of that simulation mattered.&lt;/p&gt;

&lt;p&gt;It is labeled Demo simulation. It uses the same workspace actions and shows the same operations, but it does not pretend to be real AI. Challenge V1 is a client-side application. Not a production AI backend. Not real-time collaboration. Not a governance platform.&lt;/p&gt;

&lt;p&gt;It is a verifiable workspace where WebMCP has a clear role when the runtime is available, and the product still works when the runtime is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unit tests do not prove a browser tool works
&lt;/h2&gt;

&lt;p&gt;Unit tests protected domain queries, adapters, validation, persistence, store actions, and tool input validation.&lt;/p&gt;

&lt;p&gt;That was necessary. It was not sufficient.&lt;/p&gt;

&lt;p&gt;A handler test does not prove that the browser registered a WebMCP tool. It does not prove that transport reached a workspace action. It does not prove that the human saw the result in the UI.&lt;/p&gt;

&lt;p&gt;So we built a black-box harness: &lt;code&gt;npm run test:webmcp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It starts an isolated Vite server, launches a temporary Chromium profile with WebMCP enabled, discovers all seven tools, and calls them through browser &lt;code&gt;ModelContext.executeTool()&lt;/code&gt;. Every run starts from Reset Demo. The harness intentionally repeats Finding, Constraint, and Proposal writes, checks DOM focus and Agent Activity, validates Current and Proposal, returns to Current, and confirms that the original &lt;code&gt;basket-adapter-shares-product-store&lt;/code&gt; relation is still there.&lt;/p&gt;

&lt;p&gt;Three full repetitions returned the same result.&lt;/p&gt;

&lt;p&gt;That was Checkpoint C: Current 2/2 -&amp;gt; Proposal 4/0. Current stayed unchanged. Retries were idempotent. Activity was visible. No errors.&lt;/p&gt;

&lt;p&gt;That finally felt like evidence. Not "it clicked locally." Not "the handler returns JSON." The full chain worked: WebMCP registration -&amp;gt; browser transport -&amp;gt; workspace action -&amp;gt; visible React state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Polish was not about making it pretty
&lt;/h2&gt;

&lt;p&gt;After functional freeze, we did not expand scope. We asked a different question: can someone understand the story without me narrating it?&lt;/p&gt;

&lt;p&gt;The answer was: not quite yet.&lt;/p&gt;

&lt;p&gt;HLD/LLD navigation could not look like unrelated tabs. Selecting an element had to open its own context, not accidentally show Checkout policy. Proposal action should not appear before Current validation exposes a problem. Added and removed evidence needed to be visible together. Agent Activity had to be readable without taking over the workspace. The standalone demo could not pretend it had a live agent connection.&lt;/p&gt;

&lt;p&gt;So we moved toward contextual drill-down and breadcrumb navigation. We added a neutral Policy state without selection, scoped Checkout policy, timeline Agent Activity, guided demo, and a dismissible history drawer.&lt;/p&gt;

&lt;p&gt;After successful Proposal validation, the workspace also got an implementation brief and the ability to save a Proposal as an architecture branch.&lt;/p&gt;

&lt;p&gt;Important detail: architecture branch is not a Git branch. It is a durable verified alternative inside Nivra. &lt;code&gt;current/commerce-1.35&lt;/code&gt; and &lt;code&gt;proposal/checkout-isolation&lt;/code&gt; can be compared without overwriting Current.&lt;/p&gt;

&lt;p&gt;The visual verification was practical: desktop layouts at 1440x900 and 1920x1080, no horizontal overflow, readable Checkout LLD evidence and Agent Activity, clean browser console.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production does not prove WebMCP
&lt;/h2&gt;

&lt;p&gt;After local stabilization, the app went live on Vercel: &lt;a href="https://nivra-psi.vercel.app" rel="noopener noreferrer"&gt;nivra-psi.vercel.app&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Deployment then became Git-driven: pushing to &lt;code&gt;main&lt;/code&gt; runs the production build through the connected GitHub repository. But deployment itself proves almost nothing about WebMCP.&lt;/p&gt;

&lt;p&gt;The app can load over HTTPS, the UI can look fine, and browser tool registration can still be broken. So the same black-box harness was run against the public HTTPS URL through &lt;code&gt;NIVRA_WEBMCP_URL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Three production repetitions confirmed the same behavior: all seven tools were registered, Checkout LLD evidence opened and focused, Current returned 2 passed / 2 failed, Proposal returned 4 passed / 0 failed, retries stayed idempotent, Current was not polluted by proposal-only state, and Agent Activity stayed visible without error state.&lt;/p&gt;

&lt;p&gt;By the end, there were four layers of verification: TypeScript typecheck, unit tests, and production build; manual browser checkpoints for HLD/LLD and the reasoning loop; the WebMCP-enabled Chromium black-box harness; and the same checks against production HTTPS.&lt;/p&gt;

&lt;p&gt;Only after that did I consider Nivra ready for demo and submission.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Nivra became
&lt;/h2&gt;

&lt;p&gt;Nivra started with a question: what idea is worth building for the WebMCP Challenge?&lt;/p&gt;

&lt;p&gt;By the end, the question had changed. Nivra had become a concrete system with a source of truth, a reasoning loop, browser tools, deterministic validation, and a reproducible demo state.&lt;/p&gt;

&lt;p&gt;It did not become an automatic architect.&lt;/p&gt;

&lt;p&gt;Good.&lt;/p&gt;

&lt;p&gt;It does not draw architecture instead of the human. It gives the human and the agent one shared context where they can expose a hidden dependency, define a rule, propose an alternative, and verify it without silently replacing Current Architecture.&lt;/p&gt;

&lt;p&gt;At that point, the project stopped being just a concept. The human could see the architecture. The agent could work with the same model. Deterministic checks prevented a pretty proposal from quietly becoming the current system.&lt;/p&gt;

&lt;p&gt;You can try the live app, read the source, or watch the demo here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nivra-psi.vercel.app" rel="noopener noreferrer"&gt;Live app&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/MaximusFT/nivra" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://youtu.be/BSz_Aqm4Ius" rel="noopener noreferrer"&gt;YouTube demo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next step was no longer deciding what to build. It was showing whether this combination — human, agent, WebMCP, and a shared Architecture Model — could hold up live, in front of someone who had not watched the whole thing being built.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>architecture</category>
      <category>hackathon</category>
      <category>webmcp</category>
    </item>
    <item>
      <title>What FSD Actually Fixed (and What It Didn't)</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Wed, 09 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/what-fsd-actually-fixed-and-what-it-didnt-3nfg</link>
      <guid>https://dev.to/maximusft/what-fsd-actually-fixed-and-what-it-didnt-3nfg</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9gu6hg20hbj31eamfg5o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9gu6hg20hbj31eamfg5o.png" alt="What FSD Actually Fixed (and What It Didn't)" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a long time, "frontend architecture" was almost a contradiction in terms. Backend had layers, domains, bounded contexts, decades of literature. Frontend had &lt;code&gt;components/&lt;/code&gt;, &lt;code&gt;utils/&lt;/code&gt;, and a folder called &lt;code&gt;helpers&lt;/code&gt; that everyone was quietly afraid to open. The general vibe, even among good engineers, was that the frontend didn't need real architecture — it was just where you rendered the thing the backend actually built.&lt;/p&gt;

&lt;p&gt;Feature-Sliced Design showed up into that vacuum, and I think that context matters more than the methodology's specific rules. FSD didn't just propose a folder convention. It made an argument: the frontend is complex enough, and grows unpredictably enough, that it deserves the same seriousness backend systems get. That argument is the actual contribution, and it's the reason FSD spread as fast as it did — not the specific layer names.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;FSD's real achievement wasn't a folder structure. It was convincing an entire ecosystem that the frontend was worth architecting at all.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I've &lt;a href="https://ma-x.im/blog/smeared-component" rel="noopener noreferrer"&gt;written before&lt;/a&gt; about how following FSD's rules literally, without thinking, produced one of the worse codebases I've built — a single button smeared across ten folders, each one "correctly" following the layer it was assigned to. That article was about the failure mode. This one is about something narrower: which problem FSD was actually solving, and where the credit for "clean architecture" quietly gets misattributed to a folder convention that never promised it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What FSD actually fixed
&lt;/h2&gt;

&lt;p&gt;Three things, and they're real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It gave teams a shared vocabulary for growth.&lt;/strong&gt; Before FSD, "where does this go" was answered differently by every senior engineer on the team, usually based on whatever they'd seen at their last job. FSD gives you &lt;code&gt;pages&lt;/code&gt;, &lt;code&gt;features&lt;/code&gt;, &lt;code&gt;entities&lt;/code&gt;, &lt;code&gt;shared&lt;/code&gt; — not perfect names, but names everyone on a team can agree mean the same thing. That alone kills a huge amount of bikeshedding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It made import direction a rule instead of a convention.&lt;/strong&gt; The layer hierarchy — shared can't import from entities, entities can't import from features, and so on — is enforceable with a lint rule. That's the part I actually think is underrated. Most frontend codebases don't have architectural violations because someone made a bad call; they have them because nothing stopped the bad call from compiling. FSD turned "please don't import upward" from a code review comment into a build failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It gave "the frontend has architecture" a name people could put on a job posting.&lt;/strong&gt; This sounds cynical, but I mean it as a genuine point: before FSD (and a handful of contemporaries), it was hard to even have the conversation about frontend architecture in a hiring or planning context, because there wasn't a shared reference point. Now there is. That's a real, if unglamorous, win.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it never promised to fix
&lt;/h2&gt;

&lt;p&gt;Here's where I think people — including a version of me, a few years back — get it wrong. FSD tells you &lt;em&gt;where&lt;/em&gt; a piece of code should live relative to other pieces. It does not tell you &lt;em&gt;whether the pieces you're splitting apart should have been split apart in the first place.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That distinction matters because the smeared-component failure isn't a bug in FSD. It's what happens when you use a layering convention to answer a question it was never designed to answer: how much of a single feature's logic should live together versus be distributed. FSD's layers describe &lt;em&gt;horizontal&lt;/em&gt; concerns — what kind of thing is this (a hook, a type, an API call) — and following them faithfully will happily scatter every piece of a single &lt;em&gt;vertical&lt;/em&gt; concern (one feature, one component, one story) across the whole hierarchy, because each piece genuinely does belong to a different layer by FSD's own rules.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// "Correct" FSD placement for one payment-method component
src/
  features/payment-method/model.ts // the hook and state
  entities/payment-method/types.ts // the type
  shared/hooks/useDebounce.ts // a helper it needs
  shared/api/paymentMethods.ts // the fetch call
  features/analytics/paymentTracking.ts // its analytics event

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

&lt;/div&gt;



&lt;p&gt;Every one of those placements passes an FSD lint rule. None of them help the next engineer who needs to change how the payment method selector behaves, because understanding it now requires reconstructing it from five files across three layers, in a codebase that will tell you, correctly, that nothing is architecturally wrong.&lt;/p&gt;

&lt;p&gt;FSD also doesn't fix cohesion at the &lt;em&gt;page&lt;/em&gt; level, and it doesn't fix team discipline. A team that argues about everything else will still argue about whether a given piece of logic is a &lt;code&gt;feature&lt;/code&gt; or an &lt;code&gt;entity&lt;/code&gt; — FSD gives you a vocabulary for the argument, not an answer to it. And a team with no code review discipline will produce a messy FSD codebase exactly as fast as it would have produced a messy flat one; the folders will just be tidier while it happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The credit-misattribution problem
&lt;/h2&gt;

&lt;p&gt;I think this is the actual thing worth naming: teams that adopt FSD often experience a real improvement in their codebase, and then attribute all of it to the layering. Some of the improvement is the layering. A meaningful chunk of it is something else entirely — the simple fact that adopting &lt;em&gt;any&lt;/em&gt; deliberate structure, discussed and agreed on as a team, forces the conversations that were previously being skipped. "Where does this go" gets asked out loud instead of guessed at. That conversation is valuable regardless of which methodology prompted it.&lt;/p&gt;

&lt;p&gt;Which means the honest version of "we adopted FSD and our codebase got better" is often "we adopted a shared convention, discussed it as a team, and enforced it with tooling — and FSD happened to be the convention we picked." That's still a win. It's just a different, more general win than "FSD solved our architecture problem," and conflating the two is how teams end up surprised when FSD alone doesn't prevent a smeared component, a bloated &lt;code&gt;shared/&lt;/code&gt; folder, or a &lt;code&gt;pages/&lt;/code&gt; layer that's turned into a dumping ground.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually reach for now
&lt;/h2&gt;

&lt;p&gt;I still use FSD's structure as a starting point — &lt;a href="https://ma-x.im/blog/react-playbook-code-structure" rel="noopener noreferrer"&gt;the version I use in practice&lt;/a&gt; keeps the layer names but treats them as a coarse sort, not a mandate to split every feature into its smallest constituent parts. The rule I actually enforce is closer to: a feature's pieces get distributed across layers only when another feature genuinely needs to share them. Until then, they stay together, even if that means a &lt;code&gt;features/payment-method/&lt;/code&gt; folder that internally holds its hook, its types, and its API call in one place, technically "violating" the purist reading of the layer boundaries.&lt;/p&gt;

&lt;p&gt;That's not a rejection of FSD. It's treating it as what it actually is: a good answer to "how do we agree on where things go as this project grows," and a tool that says nothing useful about "how much should stay together." Knowing which question you're asking is the part that doesn't come in the docs.&lt;/p&gt;

&lt;p&gt;If your team adopted FSD and it didn't fix the thing you hoped it would, I'd genuinely ask which of the two problems you actually had — because they need different fixes, and only one of them is solved by a folder structure.&lt;/p&gt;

</description>
      <category>react</category>
      <category>architecture</category>
      <category>fsd</category>
      <category>reactplaybook</category>
    </item>
    <item>
      <title>Checkout Isn't a Form. It's the Only Part of the App Where Architecture Has a Stopwatch on It</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Sun, 06 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/checkout-isnt-a-form-its-the-only-part-of-the-app-where-architecture-has-a-stopwatch-on-it-18l0</link>
      <guid>https://dev.to/maximusft/checkout-isnt-a-form-its-the-only-part-of-the-app-where-architecture-has-a-stopwatch-on-it-18l0</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4f1rdzqpy6t7r8p45a7n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4f1rdzqpy6t7r8p45a7n.png" alt="Checkout Isn't a Form. It's the Only Part of the App Where Architecture Has a Stopwatch on It" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've spent a real chunk of my career around purchase flows — one-click purchase, multi-step checkout, the whole family of screens between "I want this" and "I bought this." And the thing that took me the longest to internalize wasn't a pattern or a library. It was what we're actually selling in that flow.&lt;/p&gt;

&lt;p&gt;It isn't the UI. It isn't even the product, at that point — the user already decided on the product two screens ago. What we're selling is &lt;em&gt;time-to-goal&lt;/em&gt;. How fast, with how little friction, does this person get from "I'm ready to buy" to "it's done"? Every metric that actually matters downstream — conversion, cart abandonment, repeat purchases — is a proxy for that one number.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The product in a checkout flow isn't the form. It's the distance between intent and confirmation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This sounds like a UX statement, and it is one. But I want to argue it's also, maybe primarily, an &lt;em&gt;architecture&lt;/em&gt; statement — because the UX-y stuff people file under "polish" (responsiveness, one-click purchase, graceful recovery from a dropped connection) is downstream of decisions made in how the checkout is built, not decisions made in Figma.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every step is round-trip debt
&lt;/h2&gt;

&lt;p&gt;Here's the mental model I use. Every screen, every confirmation, every "are you sure" in a checkout flow is a withdrawal against the user's patience. Doesn't matter how pretty it is. A step is a step.&lt;/p&gt;

&lt;p&gt;Multi-step checkouts get justified all the time — shipping, then payment, then review — and sometimes that's genuinely the right shape. But I'd push back on treating that as free. Each transition is a network round trip if it's fetching anything, a re-render if it isn't, and either way it's a moment where the user can get pulled away, get a spinner they don't trust, or hit the back button and lose state they already entered.&lt;/p&gt;

&lt;p&gt;The architectural question isn't "how do we make step 2 nice." It's "does step 2 need to exist as a separate step at all, or is it three fields we could've asked for on step 1 without anyone noticing the difference." That's a product conversation, sure, but it's the frontend architect's job to keep asking it, because engineers are the ones who feel the cost of &lt;em&gt;not&lt;/em&gt; asking it — in state management, in the number of places cart data can go stale, in every edge case around "what if they refresh here."&lt;/p&gt;

&lt;h2&gt;
  
  
  One-click purchase is a state-management problem wearing a UX costume
&lt;/h2&gt;

&lt;p&gt;One-click purchase looks, from a design brief, like "remove the button presses." Architecturally, it's a much harder problem: you're compressing an entire multi-step flow's worth of validation, payment, and inventory checks into a single optimistic action, and you have to do it without ever showing the user a screen that says "wait."&lt;/p&gt;

&lt;p&gt;That means the hard part isn't the click. It's everything that has to already be true &lt;em&gt;before&lt;/em&gt; the click for the click to be safe:&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;OneClickEligibility&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;hasValidPaymentMethod&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;hasCompleteShippingAddress&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;itemInStock&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;priceUnchangedSinceLastSync&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="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canOneClickPurchase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OneClickEligibility&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="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasValidPaymentMethod&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasCompleteShippingAddress&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;itemInStock&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;priceUnchangedSinceLastSync&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;Everything hinges on that check being &lt;em&gt;current&lt;/em&gt; at the moment of the click, not current as of when the page loaded. If price or stock data is stale by even a few seconds, "one click" turns into "one click, then a screen apologizing that the price changed" — which is worse than a normal checkout, because you promised speed and delivered a rug-pull.&lt;/p&gt;

&lt;p&gt;So the real architectural commitment behind one-click purchase isn't a button component. It's a decision to keep a small slice of critical state (price, stock, payment validity) continuously fresh in the background, so the click can be trusted the instant it happens. That's a background-sync and cache-invalidation problem, not a UI problem, and it needs to be treated as first-class — not bolted on after the button ships.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimistic UI, but with a real rollback story
&lt;/h2&gt;

&lt;p&gt;Checkout is one of the few places where optimistic UI actually earns its complexity, because the alternative — making someone stare at a spinner while you confirm a card charge — is its own kind of failure. But optimistic UI without a serious rollback path is just a faster way to lie to the user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useSubmitOrder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;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;confirming&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;submitOrder&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="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OrderDraft&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;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;confirming&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// show success-leaning state immediately&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;confirmed&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;placeOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&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;confirmed&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;error&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;// the user already saw a hopeful state — the recovery message&lt;/span&gt;
      &lt;span class="c1"&gt;// has to explain what changed, not just that something failed&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;submitOrder&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The detail that actually matters here is the comment. If you show a confident "confirming your order" state and then it fails, the failure message can't be a generic toast. The user's mental model has already moved forward — they think they're done. Rolling that back cleanly, with a message that says what actually happened (card declined vs. item went out of stock vs. network timeout), is architecture work: it means your error states need to carry &lt;em&gt;why&lt;/em&gt;, not just &lt;em&gt;that&lt;/em&gt;, all the way from the API layer up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Responsiveness is a checkout feature, not a checkout nice-to-have
&lt;/h2&gt;

&lt;p&gt;I'd draw a hard line here: on most of an app, a layout that reflows awkwardly on a weird viewport is a bug you triage next sprint. On checkout, it's a lost sale, because a meaningful chunk of purchases happen one-handed, on a phone, possibly in a moving vehicle, possibly with one bar of signal.&lt;/p&gt;

&lt;p&gt;That changes the priority order of what you architect for. Payment fields need to work with autofill without fighting the browser. Buttons need to be large enough that a shaky hand doesn't mis-tap into "cancel." And the layout needs to survive the keyboard eating half the screen on mobile, because "the confirm button is off-screen when the keyboard is open" is a shockingly common way to lose an order that was otherwise complete.&lt;/p&gt;

&lt;p&gt;None of that is exotic. It's just a different priority order than the rest of the app gets, and if your component architecture treats checkout screens as "just more pages," they'll get the same generic responsive treatment as everything else — which is to say, good enough for a blog post, not good enough for a payment form.&lt;/p&gt;

&lt;h2&gt;
  
  
  The metric that should be driving these decisions
&lt;/h2&gt;

&lt;p&gt;Here's the part I actually want to land, because it's the thing that reframes all of the above from "best practices" into an actual measurement problem.&lt;/p&gt;

&lt;p&gt;If the product is time-to-goal, then the thing worth instrumenting isn't page views or even conversion rate in isolation — it's the &lt;em&gt;distribution&lt;/em&gt; of time between "entered checkout" and "confirmed order," broken down by step, and the drop-off at each step. That's a genuinely different question than "does this page load fast," and it points architecture in a specific direction: every step needs a timestamp, every abandonment needs a last-known-step, and every retry needs to be attributable to a cause (validation error, payment decline, network failure, user just left).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;trackCheckoutStep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CheckoutStep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;elapsedMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;advanced&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;abandoned&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;errored&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout_step&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;elapsedMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outcome&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;That's a small function. The architectural commitment behind it — instrumenting every step consistently, from the same source of truth as the state machine driving the flow, not bolted on separately by whoever remembers to add tracking — is the actual work. Get that right and you have a real answer to "where is friction actually happening," instead of a guess dressed up as a redesign.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves the architecture conversation
&lt;/h2&gt;

&lt;p&gt;None of this is about picking the right library for multi-step forms. It's about recognizing that checkout is the one part of the app where the business metric (completed purchase) and the architecture metric (time-to-goal, state consistency across steps, recovery from failure) are the same number wearing two names. Everywhere else in the app, you can separate "is this well-architected" from "does this convert." In checkout, you mostly can't — a badly architected checkout &lt;em&gt;is&lt;/em&gt; a checkout with worse conversion, because every extra round trip, every stale price, every optimistic update with no honest rollback is friction the user feels as delay between wanting the thing and having it.&lt;/p&gt;

&lt;p&gt;If you're the one arguing for a cleaner checkout architecture and getting pushback that it's "just implementation detail" — it isn't. I'd make the case that of everything in the app, this is the part where the architecture review and the conversion review should be the same meeting.&lt;/p&gt;

&lt;p&gt;I've got a lot more from this part of my career than fits in one article — one-click purchase edge cases, retry strategies for flaky payment providers, the exact way stale cart state causes support tickets. If there's a piece of this you want me to go deeper on, tell me which one and I'll write it.&lt;/p&gt;

</description>
      <category>ecommerce</category>
      <category>checkout</category>
      <category>architecture</category>
      <category>ux</category>
    </item>
    <item>
      <title>Most Advanced TypeScript Isn't Worth What It Costs</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/most-advanced-typescript-isnt-worth-what-it-costs-32l1</link>
      <guid>https://dev.to/maximusft/most-advanced-typescript-isnt-worth-what-it-costs-32l1</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzuwogavu7a54o2td3ent.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzuwogavu7a54o2td3ent.png" alt="Most Advanced TypeScript Isn't Worth What It Costs" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let me start with an admission, because it shapes everything below: I'm not especially strong at advanced TypeScript. Conditional types nested three deep, recursive template literal parsers, the kind of thing that shows up in a library's internals and gets applauded on Twitter — I read that code slowly, and I don't enjoy writing it.&lt;/p&gt;

&lt;p&gt;For a while I treated that as a gap to close. Now I mostly treat it as a constraint worth designing around, and I think that's the more useful position. Not because type-level programming isn't impressive. Because on an application codebase, a type that only one person on the team can modify is a liability wearing the costume of rigor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ma-x.im/blog/react-playbook-typescript-patterns" rel="noopener noreferrer"&gt;The earlier TypeScript article&lt;/a&gt; covered the patterns I reach for constantly — typing the API boundary, discriminated unions for async state, inference at the router. This one is about the question that comes &lt;em&gt;after&lt;/em&gt; you know the patterns: how much type complexity should a given piece of code carry, and how do you tell when you've overshot?&lt;/p&gt;

&lt;h2&gt;
  
  
  Types have a running cost
&lt;/h2&gt;

&lt;p&gt;The pitch for TypeScript is that types catch bugs. True, and I'd never go back. But the framing hides something: a type isn't a one-time purchase. It's a thing every future reader has to understand before they can safely change the code underneath it.&lt;/p&gt;

&lt;p&gt;So each type has a cost, and it shows up in three places. Someone has to read it to understand the contract. Someone has to modify it when requirements move. And when it goes wrong, someone has to decode the error message — which, for a sufficiently clever type, is a wall of text that names none of the things you actually wrote.&lt;/p&gt;

&lt;p&gt;That last one deserves more weight than it usually gets. A type that produces an unreadable error at the call site has partly defeated its own purpose. The whole point was to tell someone they made a mistake. If the message needs its own investigation, you haven't caught the bug so much as relocated it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The value of a type is not how much it proves. It's how much it proves &lt;em&gt;per unit of understanding it demands&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That ratio is the thing I actually optimize for now, and it explains why my list of "worth it" is short.&lt;/p&gt;

&lt;h2&gt;
  
  
  What earns its keep
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Discriminated unions, everywhere.&lt;/strong&gt; This is the highest-value construct in application TypeScript and it's barely "advanced" at all:&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;PolicyRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="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="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="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="nl"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="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="nl"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Four lines, and an entire class of bug is now unrepresentable — you cannot read &lt;code&gt;policies&lt;/code&gt; without first proving you're in the success branch. The compiler narrows it for you, the errors are legible, and any developer can extend it without a conversation. Cheap to read, expensive bugs prevented. That's the ratio you want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Branded types, but only where mixups are real.&lt;/strong&gt; If your codebase passes around several kinds of ID as bare strings and you've actually shipped a bug from swapping two of them, this is worth it:&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;PolicyId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;__brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PolicyId&lt;/span&gt;&lt;span class="dl"&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;CustomerId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;__brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CustomerId&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;Note the condition. &lt;em&gt;If you've actually shipped that bug.&lt;/em&gt; Branding every primitive in the app because it's theoretically safer is how you end up with casts scattered everywhere and a team quietly annoyed at you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;satisfies&lt;/code&gt; instead of annotation, when you want both.&lt;/strong&gt; This one is underused and it's almost free:&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;routePermissions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;policies&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;read&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;write&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;billing&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;read&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;satisfies&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Permission&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get the constraint checked &lt;em&gt;and&lt;/em&gt; the literal types preserved, so &lt;code&gt;routePermissions.billing&lt;/code&gt; is &lt;code&gt;['read']&lt;/code&gt; rather than a widened &lt;code&gt;Permission[]&lt;/code&gt;. No cleverness required, real inference gained.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generics on components, one parameter deep.&lt;/strong&gt; A typed &lt;code&gt;DataTable&amp;lt;TRow&amp;gt;&lt;/code&gt; that infers the row type from the data you pass it is genuinely good. Two or three interdependent type parameters with constraints referencing each other is where it stops paying, in my experience — the signature becomes the thing people copy-paste rather than read.&lt;/p&gt;

&lt;h2&gt;
  
  
  What usually doesn't
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Type-level logic that reimplements runtime logic.&lt;/strong&gt; Deep conditional types that compute a shape based on five flags. Recursive types that parse a string format into a structure. These are legitimately impressive and they belong in libraries, where a small number of maintainers absorb the complexity so thousands of users don't have to. In application code the arithmetic is inverted: your whole team pays, nobody outside benefits, and the requirements will change next quarter anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Types that exist to avoid a small runtime check.&lt;/strong&gt; Sometimes the honest answer is a validation at the boundary and a plain type afterward:&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;policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;policySchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You've now got a real guarantee — checked against actual data, not just asserted about it — and the type downstream is boring. I'd take that over an elaborate type that describes what the server &lt;em&gt;should&lt;/em&gt; send, every time. The type system can't see your API. A parser can.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generic abstractions built for one call site.&lt;/strong&gt; The rule I'd apply here is the same one that applies to component abstractions: don't generalize until you have the second case in front of you. A generic hook with three type parameters serving exactly one consumer is a puzzle you built for yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test I actually use
&lt;/h2&gt;

&lt;p&gt;When I'm not sure whether a type has gone too far, I ask two things.&lt;/p&gt;

&lt;p&gt;First: &lt;em&gt;can a mid-level developer on this team change the code this type guards, without asking me?&lt;/em&gt; If the honest answer is no, the type has become a bottleneck with my name on it. That's not safety — it's a bus factor problem I introduced on purpose.&lt;/p&gt;

&lt;p&gt;Second: &lt;em&gt;what does the error look like when someone gets it wrong?&lt;/em&gt; I'll take a slightly weaker type with a clear failure message over a stronger one that produces forty lines of inference noise. The error message is the user interface of a type, and it deserves the same consideration as any other interface.&lt;/p&gt;

&lt;p&gt;Neither test involves how much the type proves. That's deliberate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I've landed
&lt;/h2&gt;

&lt;p&gt;I used to think being weak at type-level programming meant I'd eventually hit a ceiling in TypeScript. What actually happened is that the ceiling turned out to be somewhere I don't need to go very often — and that most of the safety I care about comes from a handful of unglamorous constructs applied consistently at the right places, mostly at the edges where data enters the app.&lt;/p&gt;

&lt;p&gt;The interesting decisions aren't in the type system at all. They're about &lt;em&gt;where&lt;/em&gt; you put the boundary, what you validate, and which mistakes you've decided are worth making impossible. Once those are right, the types tend to stay simple on their own. When I find myself writing something genuinely gnarly, it's usually a sign the design underneath it is doing something it shouldn't.&lt;/p&gt;

&lt;p&gt;I'm aware this is the position of someone who isn't a type wizard, and I'd take the counter-argument seriously. So if you've got a case where deep type-level machinery genuinely paid for itself in an application — not a library — I want to see it. That's the example that would move me.&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>architecture</category>
      <category>reactplaybook</category>
    </item>
    <item>
      <title>Your Agent Instructions Are Rotting Right Now</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Tue, 01 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/your-agent-instructions-are-rotting-right-now-3e2</link>
      <guid>https://dev.to/maximusft/your-agent-instructions-are-rotting-right-now-3e2</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsiqlpjdoiitvqzgcu5kd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsiqlpjdoiitvqzgcu5kd.png" alt="Your Agent Instructions Are Rotting Right Now" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's a particular kind of file that only ever grows. You know the one. It started as a short list of project conventions for your agent, and every time something went sideways you appended a line to stop it happening again. &lt;em&gt;Always run the build before committing. Don't touch the lockfile. Use the existing helper instead of writing a new one.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every line was correct on the day it was written. That's what makes this hard to see.&lt;/p&gt;

&lt;p&gt;Because instructions aren't code. Nothing fails when they go stale. There's no red test, no type error, no failing pipeline. A rule that stopped being true six months ago sits there looking exactly like a rule that's still true, and the agent — being obedient — follows both with equal conviction.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Instructions don't break loudly. They just quietly stop describing your project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://ma-x.im/blog/agent-playbook-context-is-the-product" rel="noopener noreferrer"&gt;The context is the product&lt;/a&gt; made the case that what you feed an agent matters more than which model you picked. This is the uncomfortable follow-up: that context is an artifact you own, and artifacts you own need maintenance. Nobody budgets for maintaining a text file.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the rot actually happens
&lt;/h2&gt;

&lt;p&gt;It's never one dramatic mistake. It's four small, reasonable things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The project moves and the file doesn't.&lt;/strong&gt; You migrate from one test runner to another, rename a directory, drop a library. The code changes in one commit. The instruction describing the old world changes in... no commit, because nobody thought about it. Now the agent is confidently steering toward a folder that isn't there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rules get added to fix single incidents.&lt;/strong&gt; The agent did something dumb once, so you wrote a rule. The rule is narrow, situational, and phrased as a universal law. Twenty of those later, you've encoded a list of past accidents rather than a description of how the project works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing ever gets deleted.&lt;/strong&gt; Deleting a rule feels risky — what if it was load-bearing? So the file only accretes. And because instructions usually sit near the top of the agent's context, every dead line is taking up room that live information could be using.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contradictions arrive silently.&lt;/strong&gt; Rule 4 says prefer the shared utility. Rule 19, added months later by someone solving a different problem, says write it locally in the feature folder. Both are in the file. The agent picks one, more or less arbitrarily, and now its behavior looks random — which is the symptom people usually misdiagnose as the model being unreliable.&lt;/p&gt;

&lt;p&gt;That last one is worth sitting with. When an agent behaves inconsistently across similar tasks, the instinct is to blame the model or add &lt;em&gt;another&lt;/em&gt; rule. Often the real cause is that you've given it two rules and asked it to guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptoms, before you go looking
&lt;/h2&gt;

&lt;p&gt;You can usually feel this before you can point at it.&lt;/p&gt;

&lt;p&gt;The agent starts doing things you don't remember asking for, and when you grep the instructions, there it is — a line from months ago you'd completely forgotten writing. Or it does the right thing four times out of five, and the fifth is a coin flip. Or you find yourself correcting the same class of thing in review over and over, adding a clarification each time, and the clarifications aren't helping. That's the tell: if adding rules isn't improving behavior, the problem isn't a missing rule. It's the pile.&lt;/p&gt;

&lt;p&gt;Honestly, the strongest signal is simpler. When was the last time you read your instructions file top to bottom? If you can't remember, you're not maintaining a document, you're maintaining a sediment layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat it like code, because it is
&lt;/h2&gt;

&lt;p&gt;The fix isn't clever. It's just deciding that this file has an owner and a lifecycle, the same as any other part of the system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read it end to end on a schedule.&lt;/strong&gt; Not when something breaks — on a cadence. It takes ten minutes. You will find at least one line that describes a project that no longer exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delete aggressively.&lt;/strong&gt; This is the part people won't do, so let me put it plainly: a rule you can't justify today is doing damage today. It's consuming context, and it's a coin-flip waiting to happen when it eventually contradicts something newer. If you're wrong and it mattered, you'll find out fast and can put it back in thirty seconds. That's a cheap mistake. Carrying twenty dead rules is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefer describing the system over listing incidents.&lt;/strong&gt;"Feature code lives under &lt;code&gt;src/features/{feature}&lt;/code&gt;, and anything shared moves to &lt;code&gt;src/shared&lt;/code&gt; only when a second feature needs it" is worth ten rules about specific files. Principles survive refactors. Incident notes don't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hunt contradictions directly.&lt;/strong&gt; When you review, don't just ask &lt;em&gt;is this line true?&lt;/em&gt; Ask &lt;em&gt;does this line disagree with another line?&lt;/em&gt; Contradictions are the expensive failure here, and they're invisible unless you're specifically looking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update instructions in the same commit as the change.&lt;/strong&gt; You renamed the folder — the rule mentioning that folder is part of the rename. This is the one habit that stops rot at the source, and it costs nothing once it's automatic.&lt;/p&gt;

&lt;h2&gt;
  
  
  I've had to redo this myself
&lt;/h2&gt;

&lt;p&gt;I'm not writing this from the outside. The memory system on this site is exactly this problem in a different shape — a set of files an agent reads before it does anything, describing conventions and decisions.&lt;/p&gt;

&lt;p&gt;I wrote up the first version, published it, and then &lt;a href="https://ma-x.im/blog/ai-agent-memory-redesign" rel="noopener noreferrer"&gt;redesigned the whole thing after a few weeks of actually using it&lt;/a&gt;. Not because the original idea was wrong, but because the first design let information accumulate without ever forcing anything out. The rules that ended up mattering were about &lt;em&gt;overwriting&lt;/em&gt; rather than appending: this file gets rewritten in full, that entry gets edited in place rather than duplicated, completed items get deleted rather than archived. Not because deletion is elegant. Because anything that only grows eventually stops being read — by me, and by the agent.&lt;/p&gt;

&lt;p&gt;That's the lesson I'd extract for instructions generally. The interesting design question isn't what to write down. It's what forces something to leave.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small is a feature
&lt;/h2&gt;

&lt;p&gt;There's a version of this article that ends with "so audit your rules regularly," and that's true but weak. The stronger claim is that the size of an instruction file is itself a quality signal, in the wrong direction.&lt;/p&gt;

&lt;p&gt;A short, current, internally consistent set of rules beats a comprehensive one, every time. The agent reads all of it. You can hold all of it in your head. When something's wrong you can find it. A long file fails on all three, and it fails invisibly, which is the worst property a piece of configuration can have.&lt;/p&gt;

&lt;p&gt;So the question I'd ask about your instructions isn't whether they're thorough. It's whether you'd be comfortable reading them out loud to a new engineer joining tomorrow — and how many lines you'd catch yourself apologizing for on the way through.&lt;/p&gt;

&lt;p&gt;If you've found a maintenance rhythm that actually sticks — something better than "review it when the agent embarrasses you" — I'd like to hear it. That's the part I'm still refining.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>architecture</category>
      <category>theagentplaybook</category>
    </item>
    <item>
      <title>They Asked Me to Build a Slider. It Was a Much Better Question Than I Thought.</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Sun, 30 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/they-asked-me-to-build-a-slider-it-was-a-much-better-question-than-i-thought-1hk7</link>
      <guid>https://dev.to/maximusft/they-asked-me-to-build-a-slider-it-was-a-much-better-question-than-i-thought-1hk7</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk9gk87rx8fulb66f9kye.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk9gk87rx8fulb66f9kye.png" alt="They Asked Me to Build a Slider. It Was a Much Better Question Than I Thought." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I sat down for a technical interview recently and got handed this: here's a React page, here are ten image URLs from the internet, build a slider. Next button, previous button, a row of dots underneath. Click a dot, jump straight to that slide. No animation needed.&lt;/p&gt;

&lt;p&gt;My honest first reaction was &lt;em&gt;is this it?&lt;/em&gt; I'm an architect. I'd come in expecting to defend some structural decision, argue about boundaries, maybe get grilled on a system design. Instead I got a task I'd have called kindergarten-level on paper.&lt;/p&gt;

&lt;p&gt;Then I started typing, and about thirty seconds in I got genuinely interested — not in the slider, in what the slider was doing to me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first thirty seconds
&lt;/h2&gt;

&lt;p&gt;Before writing anything I asked questions. Do you want keyboard support? Should it wrap around? Do the images need preloading? Some of those got a &lt;em&gt;no, don't worry about it&lt;/em&gt;. Fine — that's an answer, and now the scope is pinned down instead of assumed.&lt;/p&gt;

&lt;p&gt;Then I opened the component and the first thing I wrote was state:&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;currentSlide&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCurrentSlide&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;One value. That's all this thing needs to know about itself — which slide is showing. Everything else is derived.&lt;/p&gt;

&lt;p&gt;And the second thing I wrote was three empty functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleNext&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;handlePrevious&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;handleSelectSlide&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&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="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;Empty. Nothing inside them. I hadn't decided the boundary behavior yet, hadn't written a single &lt;code&gt;div&lt;/code&gt;. I just named the three things this component can &lt;em&gt;do&lt;/em&gt;, and then kept going.&lt;/p&gt;

&lt;p&gt;I want to be careful here, because this is the part I actually find interesting and it would be easy to oversell. I'm not claiming that's the One True Order. But sitting there, watching myself do it, I realized I had described the entire component — its state and its complete behavior surface — before rendering anything. The markup afterwards was almost mechanical. There was nothing left to decide.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The interesting part of a trivial task isn't whether you finish it. It's what you reach for first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The same task, three ways
&lt;/h2&gt;

&lt;p&gt;I've never sat on the other side of the table scoring people on this, so take what follows as a hypothesis rather than a verdict. But I think this task separates approaches cleanly, and the separation has nothing to do with knowing React.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start from the markup.&lt;/strong&gt; Drop in a &lt;code&gt;div&lt;/code&gt;, put an &lt;code&gt;img&lt;/code&gt; in it, add two buttons. Then wire up the next button — oh, that needs a handler, scroll back up, write one. That needs state — scroll up again, add &lt;code&gt;useState&lt;/code&gt;. Then the dots, which need to know the active index, which means going back to think about what the index actually means. The code arrives, but it arrives by accretion, each piece pulling the previous one apart a little.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start from the pieces, one at a time.&lt;/strong&gt; State first, then a handler, then the markup for that handler, then the next handler. Better — but each behavior is decided in isolation, so the boundary rules ("what happens at the last slide?") get discovered one at a time, in the middle of writing JSX, when you're least set up to think about them consistently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start from state and behavior as a set.&lt;/strong&gt; One &lt;code&gt;useState&lt;/code&gt;, three named handlers, all declared before anything renders. The boundary question surfaces immediately and gets answered once, for all three, because they're sitting right next to each other. Then you render.&lt;/p&gt;

&lt;p&gt;The difference isn't skill with React. All three people finish. The difference is whether the shape of the component was decided up front or discovered by bumping into it, and &lt;em&gt;that&lt;/em&gt; is the thing this dumb little task exposes with surprising precision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then they made it worse, and that's when it got good
&lt;/h2&gt;

&lt;p&gt;Part two: show three images per page instead of one. And add looping — from the last page, next goes to the first; from the first, previous goes to the last.&lt;/p&gt;

&lt;p&gt;That sounds like a small tweak. It isn't, and this is the bit I enjoyed.&lt;/p&gt;

&lt;p&gt;In the one-image version, the natural move at the end is to disable the button:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleNext&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;currentSlide&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;images&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  Next
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The boundary is handled in the &lt;em&gt;markup&lt;/em&gt;, as a disabled state. Now change the rules. With three-per-page and wrapping, "the end" isn't the last image anymore, it's the last &lt;em&gt;page&lt;/em&gt; — and there's no such thing as the end, because it wraps. The button is never disabled. The logic moves out of the JSX and into the handler:&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;pageCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;images&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;imagesPerPage&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;handleNext&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="nf"&gt;setCurrentPage&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="o"&gt;=&amp;gt;&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="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="nx"&gt;pageCount&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;handlePrevious&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="nf"&gt;setCurrentPage&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="o"&gt;=&amp;gt;&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="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;pageCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;pageCount&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;Two things changed underneath, and neither is visible in the requirement as stated. The unit of state stopped being &lt;em&gt;which image&lt;/em&gt; and became &lt;em&gt;which page&lt;/em&gt;, which is a different concept that happens to look the same on screen with ten images and one per page. And the boundary rule migrated from a rendering concern to a behavioral one.&lt;/p&gt;

&lt;p&gt;If your first version had the wrapping logic smeared across the JSX, this is where you pay. If it was already sitting inside three named handlers, you edit two lines.&lt;/p&gt;

&lt;p&gt;Ten images, three per page, and suddenly the task has an actual opinion about your first draft.&lt;/p&gt;

&lt;h2&gt;
  
  
  Say it out loud or none of this counts
&lt;/h2&gt;

&lt;p&gt;Here's the thing that makes the whole exercise work, and it's not the code.&lt;/p&gt;

&lt;p&gt;Nobody watching can see you reason. They see a cursor. So I narrated the entire time: &lt;em&gt;I'll start with state — I think all I need to store is which slide is selected. Now the handlers. I'm not going to build arrow-key navigation unless you want it, just next and previous buttons.&lt;/em&gt; And the answer came back: yeah, that's fine, don't bother.&lt;/p&gt;

&lt;p&gt;That exchange took four seconds and it removed a chunk of work I would otherwise have guessed at. More importantly, it made my reasoning visible. Someone watching me could tell &lt;em&gt;why&lt;/em&gt; the code was landing in that order, which is information they cannot get any other way.&lt;/p&gt;

&lt;p&gt;This is the same argument I made in &lt;a href="https://ma-x.im/blog/interviewing-senior-engineers" rel="noopener noreferrer"&gt;the piece on interviewing senior engineers&lt;/a&gt; from the interviewer's side: the signal is in the reasoning, not the artifact. Sitting on the candidate side of it, I'd put it even more bluntly. If you think silently and produce correct code, you've handed over the least interesting half of what you know.&lt;/p&gt;

&lt;h2&gt;
  
  
  And yes, I used Google
&lt;/h2&gt;

&lt;p&gt;At one point I needed to iterate a fixed number of times to render the dots. The simplest thing is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nc"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pageCount&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&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="nc"&gt;Dot&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;active&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;currentPage&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleSelectSlide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;))}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;And I blanked on it. Couldn't remember how to make an empty array of a given length. So I asked whether I could look it up, they said sure, I looked it up, and moved on.&lt;/p&gt;

&lt;p&gt;I'd like that to be unremarkable, because it should be. Forgetting a piece of syntax you type maybe twice a year says nothing about anything. What would actually have mattered is if I hadn't known &lt;em&gt;what I was reaching for&lt;/em&gt; — that I needed a fixed-length iteration, that the dots derive from page count rather than being their own state. That part I never doubted. The incantation to produce the array is a lookup.&lt;/p&gt;

&lt;p&gt;The failure mode isn't forgetting an API. It's not knowing what shape of thing you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;p&gt;I don't know how the interview went. Genuinely — no result yet, and I'm writing this without one.&lt;/p&gt;

&lt;p&gt;But I came away thinking the task was much smarter than it looked, and I'd been slightly snobbish about it in the first minute. A hard problem mostly tells you whether someone has seen that problem before. A trivial problem, watched closely, tells you the order someone's mind moves in — and then a small twist tells you whether their first draft was structured or just correct.&lt;/p&gt;

&lt;p&gt;The slider was never the point. It was a transparent object they could watch me think through, and it cost them ten minutes to set up.&lt;/p&gt;

&lt;p&gt;If you're interviewing people and reaching for something elaborate, I'd at least consider the opposite: take something almost insultingly simple, watch the first thirty seconds, then change one requirement. And if you've been on the receiving end of a task like this and read it as disrespect — I did too, briefly. I'd like to hear whether you changed your mind the way I did.&lt;/p&gt;

</description>
      <category>interviewing</category>
      <category>career</category>
      <category>react</category>
      <category>engineeringculture</category>
    </item>
    <item>
      <title>React Gives You the Loading State. It Does Not Give You Cancellation.</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Fri, 28 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/react-gives-you-the-loading-state-it-does-not-give-you-cancellation-53g2</link>
      <guid>https://dev.to/maximusft/react-gives-you-the-loading-state-it-does-not-give-you-cancellation-53g2</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs91hbh1jbzggt5m0g8ad.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs91hbh1jbzggt5m0g8ad.png" alt="React Gives You the Loading State. It Does Not Give You Cancellation." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://ma-x.im/blog/react-playbook-error-handling" rel="noopener noreferrer"&gt;error handling article&lt;/a&gt; argued that failures are a product decision — what breaks, how loudly, and what the user does next. There's one failure mode it didn't cover, and it's the one that produces the weirdest bug reports. Not "the request failed." The opposite: the request &lt;em&gt;succeeded&lt;/em&gt;, just too late, and nobody wanted the answer anymore.&lt;/p&gt;

&lt;p&gt;React 19 made the front half of this much nicer. &lt;code&gt;useActionState&lt;/code&gt; gives you the pending flag without a single &lt;code&gt;useState&lt;/code&gt;:&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;submitSearch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isPending&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useActionState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchPoliciesAction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Three values, one line, and the loading state is handled. That used to be four lines of ceremony and at least one bug where &lt;code&gt;setLoading(false)&lt;/code&gt; didn't run on the error path. Real improvement.&lt;/p&gt;

&lt;p&gt;But look at what &lt;code&gt;isPending&lt;/code&gt; actually tells you. It says React is still waiting on &lt;em&gt;this&lt;/em&gt; action. It says nothing about the fetch you fired ninety milliseconds ago that's still in flight somewhere over the Atlantic. React flipped a boolean. The network doesn't know that happened.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A spinner that stops is a UI event. A request that stops is a network event. React only does the first one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The bug that survives &lt;code&gt;isPending&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Type into a search box. Every keystroke fires a request. Responses come back whenever they feel like it — the one for "poli" leaves first, the one for "policy" leaves second and returns first, then "poli" lands and overwrites it. Now the input says &lt;em&gt;policy&lt;/em&gt; and the list below shows results for &lt;em&gt;poli&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The UI is not loading. The UI is not erroring. The UI is confidently wrong, and it stays wrong until the user touches something.&lt;/p&gt;

&lt;p&gt;This is a race, and pending state cannot fix a race. &lt;code&gt;isPending&lt;/code&gt; was true, then false, exactly as designed. What you needed was for the earlier request to &lt;em&gt;stop existing&lt;/em&gt; the moment it stopped mattering. That's the job AbortController does, and it's the reason I think it deserves a chapter of its own rather than a footnote in a data-fetching post.&lt;/p&gt;

&lt;p&gt;Honestly, I think the main reason this stays exotic is the name. &lt;code&gt;AbortController&lt;/code&gt; sounds like something you'd find in an operating systems textbook. The actual API is three things.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole API, in about ten lines
&lt;/h2&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;controller&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;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/policies&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;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;That's it. You make a controller, you hand its &lt;code&gt;signal&lt;/code&gt; to whatever is doing the async work, and calling &lt;code&gt;abort()&lt;/code&gt; tells that work to give up. The pending &lt;code&gt;fetch&lt;/code&gt; rejects immediately.&lt;/p&gt;

&lt;p&gt;Two details worth knowing up front, because they're where people trip.&lt;/p&gt;

&lt;p&gt;First, a signal is single-use. Once a controller is aborted, it stays aborted forever — you don't reset it, you make a new one. One controller per request, not one per component.&lt;/p&gt;

&lt;p&gt;Second, aborting rejects the promise. It doesn't quietly resolve to nothing. Which brings us to the mistake I'd bet is the single most common one in this whole area.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cancellation is not a failure
&lt;/h2&gt;

&lt;p&gt;When you abort a fetch, it throws. If you have a normal &lt;code&gt;try/catch&lt;/code&gt; around it, that &lt;code&gt;catch&lt;/code&gt; runs, and unless you say otherwise the user gets an error state for something &lt;em&gt;you&lt;/em&gt; deliberately caused.&lt;/p&gt;

&lt;p&gt;I've seen this produce red toasts on perfectly healthy apps. Navigate away from a page mid-load, and the app cheerfully informs you that something went wrong. Nothing went wrong. You left.&lt;/p&gt;

&lt;p&gt;So the rule that matters more than any other in this article:&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="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;error&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;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;DOMException&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AbortError&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;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// we cancelled this on purpose — not a user-facing failure&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&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;An abort is an instruction you gave. It should never reach your error UI, your toast system, or Sentry. Treat it as control flow, not as a fault.&lt;/p&gt;

&lt;p&gt;There's a newer, cleaner way to express the same check if you already have the signal at hand:&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aborted&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same idea, less &lt;code&gt;instanceof&lt;/code&gt; archaeology. Both are fine. What's not fine is letting the abort fall through into the same branch as a 500.&lt;/p&gt;

&lt;h2&gt;
  
  
  The small version that covers most cases
&lt;/h2&gt;

&lt;p&gt;Here's the shape I'd reach for by default. A ref holding the current controller, aborted right before the next request starts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PolicySearchPanel&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;inFlight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AbortController&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;&amp;gt;&lt;/span&gt;&lt;span class="p"&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;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;searchPolicies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isPending&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useActionState&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;_previous&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;[]&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="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormData&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;inFlight&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="nf"&gt;abort&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;controller&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;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;inFlight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;controller&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;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/policies?query=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="p"&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="nf"&gt;useEffect&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;gt;&lt;/span&gt; &lt;span class="nx"&gt;inFlight&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="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;searchPolicies&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"query"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isPending&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Search&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Two lines do the real work. &lt;code&gt;inFlight.current?.abort()&lt;/code&gt; kills the previous request before starting a new one, so the stale response can never land. The &lt;code&gt;useEffect&lt;/code&gt; cleanup aborts on unmount, so navigating away doesn't leave a request writing into a component that no longer exists.&lt;/p&gt;

&lt;p&gt;That's the universal, boring version. It isn't clever and it doesn't need to be. Most cancellation bugs I can think of are covered by exactly those two moments: &lt;em&gt;something newer started&lt;/em&gt;, and &lt;em&gt;the thing that wanted this is gone&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Notice what &lt;code&gt;useActionState&lt;/code&gt; is and isn't doing here. It owns &lt;code&gt;isPending&lt;/code&gt; and the result value, which is genuinely less code than before. The cancellation is still entirely yours. The new hooks didn't make this obsolete — they made the missing half more visible, because now the only thing you're hand-rolling &lt;em&gt;is&lt;/em&gt; the abort.&lt;/p&gt;

&lt;h2&gt;
  
  
  The signal has to reach the bottom
&lt;/h2&gt;

&lt;p&gt;Here's where this stops being a component concern and becomes an architecture one.&lt;/p&gt;

&lt;p&gt;Most apps don't call &lt;code&gt;fetch&lt;/code&gt; in components. They call &lt;code&gt;policiesApi.search(query)&lt;/code&gt;, which calls a shared &lt;code&gt;httpClient&lt;/code&gt;, which eventually calls &lt;code&gt;fetch&lt;/code&gt;. If any layer in that chain doesn't take a signal, cancellation dies there — and you'll be sitting in the component wondering why abort does nothing.&lt;/p&gt;

&lt;p&gt;So the API layer needs to pass it through, all the way down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;searchPolicies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt; &lt;span class="p"&gt;},&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;Policy&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;response&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;httpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/policies?query=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&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="nx"&gt;signal&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;response&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="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;An optional &lt;code&gt;signal&lt;/code&gt; on every async function that touches the network. It costs nothing at the call sites that don't care, and it's the difference between cancellation being available and being theoretically available.&lt;/p&gt;

&lt;p&gt;This is also why I'd rather teach the primitive than the library helper. TanStack Query already hands your query function a signal — you just have to &lt;em&gt;use&lt;/em&gt; it:&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="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;policies&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;query&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="nx"&gt;signal&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;searchPolicies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;That signal is there in every TanStack Query app in the world, and a lot of them pass it nowhere. The library already solved the plumbing. If your own layers drop the signal on the floor, the plumbing has nothing to plug into.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two extras that are worth knowing
&lt;/h2&gt;

&lt;p&gt;Timeouts, without the &lt;code&gt;setTimeout&lt;/code&gt; dance:&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;And combining reasons to stop — say, "cancel if the user navigates away &lt;em&gt;or&lt;/em&gt; if eight seconds pass":&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;signal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;A timeout abort surfaces as a &lt;code&gt;TimeoutError&lt;/code&gt; rather than an &lt;code&gt;AbortError&lt;/code&gt;, which is exactly what you want: one of those deserves a message to the user, the other doesn't. That distinction is the whole game — &lt;em&gt;why&lt;/em&gt; did this stop, and does the person staring at the screen need to know?&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually remember
&lt;/h2&gt;

&lt;p&gt;Strip it down and there are maybe five things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One controller per request. Never reuse an aborted one.&lt;/li&gt;
&lt;li&gt;Abort the previous request before starting the next one.&lt;/li&gt;
&lt;li&gt;Abort on unmount.&lt;/li&gt;
&lt;li&gt;Never show an abort in the UI. It's control flow, not an error.&lt;/li&gt;
&lt;li&gt;Accept an optional &lt;code&gt;signal&lt;/code&gt; in every async function that reaches the network, or the chain breaks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing there is advanced. It's five habits, and once they're muscle memory you stop writing an entire category of bug — the stale-write, the ghost update, the error toast on a page you already left.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I keep coming back to
&lt;/h2&gt;

&lt;p&gt;What strikes me about React 19's async story is how much it improved the &lt;em&gt;visible&lt;/em&gt; half of the problem. Pending state used to be where the boilerplate lived, and now it mostly isn't. That's real, and I'd take it every time.&lt;/p&gt;

&lt;p&gt;But it makes the asymmetry sharper. The framework got better at telling you it's waiting, and no better at stopping the thing it's waiting for — because it can't. React owns the component tree. It doesn't own the network. The moment you fire a request, you've created something that outlives React's opinion of it, and the only way to reel that back in is to have kept a handle on it.&lt;/p&gt;

&lt;p&gt;AbortController is that handle. It's fifteen lines of very unglamorous code, and I'd argue it's the difference between an app that &lt;em&gt;looks&lt;/em&gt; responsive and one that's actually consistent with what the user is doing right now.&lt;/p&gt;

&lt;p&gt;If you've got a cancellation pattern that's held up better than the ref-and-cleanup version above — especially in bigger apps where requests fan out across several layers — I'd genuinely like to see it. Send it over.&lt;/p&gt;

</description>
      <category>react</category>
      <category>errors</category>
      <category>abortcontroller</category>
      <category>async</category>
    </item>
  </channel>
</rss>
