<?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: Can Ceylan</title>
    <description>The latest articles on DEV Community by Can Ceylan (@canceylan1988).</description>
    <link>https://dev.to/canceylan1988</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3861469%2F5a1153d6-d767-404d-8731-acf1033c9807.png</url>
      <title>DEV Community: Can Ceylan</title>
      <link>https://dev.to/canceylan1988</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/canceylan1988"/>
    <language>en</language>
    <item>
      <title>Use a working-memory file as the handoff layer between AI coding sessions</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Fri, 22 May 2026 07:58:00 +0000</pubDate>
      <link>https://dev.to/canceylan1988/use-a-working-memory-file-as-the-handoff-layer-between-ai-coding-sessions-5cl7</link>
      <guid>https://dev.to/canceylan1988/use-a-working-memory-file-as-the-handoff-layer-between-ai-coding-sessions-5cl7</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;AI coding assistants are powerful — but stateless. Every new session starts cold. The agent doesn't know what was decided yesterday, why a particular approach was chosen, or what not to touch while something else is in progress.&lt;/p&gt;

&lt;p&gt;This creates invisible risk: the same wrong decision gets made twice, context gets re-explained from scratch every session, and two agents working in parallel can silently collide on the same files.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;Keep a &lt;code&gt;docs/working-memory.md&lt;/code&gt; file in the project root. It is not documentation. It is not a changelog. It is the shared, current-state brain of the project — meant to be read at the start of every session before any code is written.&lt;/p&gt;

&lt;p&gt;The file answers four questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What is the current architecture?&lt;/strong&gt; — the decisions that must not be undone&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What is in progress right now?&lt;/strong&gt; — the protected work that should not be touched&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What is next?&lt;/strong&gt; — the agreed next step, not a wishlist&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What changed recently and why?&lt;/strong&gt; — enough context to reconstruct the reasoning
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Current Architecture&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Articles stored in Vercel KV via lib/content-db.ts
&lt;span class="p"&gt;-&lt;/span&gt; Git is the source of truth for code, not for content

&lt;span class="gu"&gt;## Next 2 Steps&lt;/span&gt;

&lt;span class="gu"&gt;### 1. Surface asset readiness in the existing UI&lt;/span&gt;
What to do: show distributionState.assets in article editor cards
What not to touch: do not replace the Articles tab structure

&lt;span class="gu"&gt;### 2. Extend provider fallback to remaining generation routes&lt;/span&gt;
What not to touch: do not add per-run ceremony

&lt;span class="gu"&gt;## Recent Decisions&lt;/span&gt;

&lt;span class="gu"&gt;### 2026-04-24&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; distributionState is now the per-article workflow record in KV
&lt;span class="p"&gt;-&lt;/span&gt; Lazy backfill: if missing, it is built on first read from socialPosts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The update rule
&lt;/h2&gt;

&lt;p&gt;The file only works if it stays current. The rule is simple: whenever a non-trivial decision is made, it gets written here in the same session — before the session ends.&lt;/p&gt;

&lt;p&gt;Prefer facts over plans:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what changed&lt;/li&gt;
&lt;li&gt;why it changed&lt;/li&gt;
&lt;li&gt;what must not be broken next time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid aspirational entries. If something was decided but not yet built, it belongs in a backlog, not here.&lt;/p&gt;

&lt;h2&gt;
  
  
  For multi-agent workflows
&lt;/h2&gt;

&lt;p&gt;When two agents (or a human and an agent) are working on the same codebase simultaneously, the working-memory file becomes a coordination layer.&lt;/p&gt;

&lt;p&gt;Designate a &lt;strong&gt;protected lane&lt;/strong&gt; — files and contracts that only one agent touches at a time. List what the other agent should not touch. This prevents merge collisions without requiring real-time communication.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Parallel Lane For Claude&lt;/span&gt;

Claude can work on these areas while Codex works on the protected lane:
&lt;span class="p"&gt;1.&lt;/span&gt; test coverage
&lt;span class="p"&gt;2.&lt;/span&gt; documentation hygiene
&lt;span class="p"&gt;3.&lt;/span&gt; UI copy polish

Claude should not touch:
&lt;span class="p"&gt;-&lt;/span&gt; app/admin/page.tsx workflow rendering rewires
&lt;span class="p"&gt;-&lt;/span&gt; shared distributionState contract changes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The working-memory file is where both agents agree on the boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a flat file beats a ticket system for this
&lt;/h2&gt;

&lt;p&gt;Ticket systems are great for task tracking. They are poor at preserving architectural reasoning in a form that an AI agent can read at the start of a session with zero additional context.&lt;/p&gt;

&lt;p&gt;A flat Markdown file in the repo has three advantages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It is always co-located with the code — no separate system to open&lt;/li&gt;
&lt;li&gt;It can be read by an AI agent as part of the first tool call of a session&lt;/li&gt;
&lt;li&gt;It can be committed alongside code changes, making the decision and the implementation visible in the same diff&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The file does not replace PRs, commit messages, or decision logs. It is the short-term working memory that keeps sessions coherent until decisions stabilize into those longer-lived artifacts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anti-patterns to avoid
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Making it too long.&lt;/strong&gt; Once it exceeds ~200 lines, agents start missing the critical parts. Keep it scannable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using it as a changelog.&lt;/strong&gt; Recent decisions should drop off after they are no longer at risk of being undone. Rotate old entries out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skipping the update.&lt;/strong&gt; The file is worthless if it is three sessions out of date. The update rule must be treated as non-negotiable, not optional.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>aitools</category>
    </item>
    <item>
      <title>Sequential vs Parallel Execution: when faster is the wrong answer</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Fri, 22 May 2026 07:57:43 +0000</pubDate>
      <link>https://dev.to/canceylan1988/sequential-vs-parallel-execution-when-faster-is-the-wrong-answer-2p1h</link>
      <guid>https://dev.to/canceylan1988/sequential-vs-parallel-execution-when-faster-is-the-wrong-answer-2p1h</guid>
      <description>&lt;p&gt;Every developer's first instinct when they see a loop is: &lt;em&gt;can I run this in parallel?&lt;/em&gt; Parallel means faster. And faster is better. Right?&lt;/p&gt;

&lt;p&gt;Not always.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the terms actually mean
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Sequential execution&lt;/strong&gt; means tasks run one after another — task B starts only when task A finishes. Think of a single cashier at a supermarket.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parallel execution&lt;/strong&gt; means tasks run simultaneously — A and B both run at the same time. Think of ten cashiers working at once.&lt;/p&gt;

&lt;p&gt;Parallel is faster. Sequential is safer and more predictable. Choosing between them is a tradeoff, not a free upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hidden cost of parallel
&lt;/h2&gt;

&lt;p&gt;When you run things in parallel, you introduce a new problem: &lt;strong&gt;shared state&lt;/strong&gt;. What happens when two tasks try to write to the same database row at the same time? What happens when two requests share the same session cookie?&lt;/p&gt;

&lt;p&gt;The short answer: unpredictable things. Race conditions. Corrupted data. Bans.&lt;/p&gt;

&lt;p&gt;This isn't theoretical. When building a price monitoring tool for a European consumer marketplace, the temptation was to parallelize keyword requests — hit 10 searches at once instead of one every 3 seconds. It would have been 10x faster. It also would have triggered the platform's anti-bot system within minutes.&lt;/p&gt;

&lt;p&gt;Most large European consumer platforms use bot-detection layers that monitor for non-human traffic patterns. A human browsing a marketplace would never fire 10 requests per second. The moment your code does, you're flagged.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule
&lt;/h2&gt;

&lt;p&gt;Sequential is the right default when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're hitting an &lt;strong&gt;external service with rate limiting or anti-bot&lt;/strong&gt; (most scrapers, payment APIs, social APIs)&lt;/li&gt;
&lt;li&gt;Tasks &lt;strong&gt;share a stateful session&lt;/strong&gt; — cookies, auth tokens, database connections&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Order matters&lt;/strong&gt; — step 2 depends on the result of step 1&lt;/li&gt;
&lt;li&gt;You're writing to a resource that &lt;strong&gt;doesn't support concurrent writes&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Parallel is the right choice when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tasks are &lt;strong&gt;truly independent&lt;/strong&gt; with no shared state&lt;/li&gt;
&lt;li&gt;You're hitting &lt;strong&gt;services you control&lt;/strong&gt; (your own API, your own database)&lt;/li&gt;
&lt;li&gt;The service &lt;strong&gt;explicitly supports high concurrency&lt;/strong&gt; (most cloud APIs with documented rate limits)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Throughput is the priority&lt;/strong&gt; and you've verified safety&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  In practice
&lt;/h2&gt;

&lt;p&gt;A scraper that respects these rules runs sequentially with a fixed pause between requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;search&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;searches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;scrape_keyword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;save_results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# politeness delay
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;asyncio.gather()&lt;/code&gt;. No &lt;code&gt;ThreadPoolExecutor&lt;/code&gt;. The delay is the feature, not a bug.&lt;/p&gt;

&lt;p&gt;If the platform starts returning errors, the fix is to &lt;em&gt;increase&lt;/em&gt; the delay — not to add clever workarounds or reduce it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The professional term
&lt;/h2&gt;

&lt;p&gt;This tradeoff is often called the &lt;strong&gt;throughput vs. safety tradeoff&lt;/strong&gt; in distributed systems. In scraping contexts, the pause between requests is called a &lt;strong&gt;politeness delay&lt;/strong&gt; or &lt;strong&gt;request throttling&lt;/strong&gt;. The broader pattern of slowing requests to avoid detection is &lt;strong&gt;rate limiting&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When you see these terms in documentation, they're telling you: this service expects you to be sequential.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>architecture</category>
    </item>
    <item>
      <title>The data isolation audit: every endpoint must be scoped to the requesting user</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Fri, 22 May 2026 07:57:26 +0000</pubDate>
      <link>https://dev.to/canceylan1988/the-data-isolation-audit-every-endpoint-must-be-scoped-to-the-requesting-user-370k</link>
      <guid>https://dev.to/canceylan1988/the-data-isolation-audit-every-endpoint-must-be-scoped-to-the-requesting-user-370k</guid>
      <description>&lt;h2&gt;
  
  
  The bug that's easy to miss in review
&lt;/h2&gt;

&lt;p&gt;You're building a multi-tenant application. A user can see their own data, not anyone else's.&lt;/p&gt;

&lt;p&gt;You add a new endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@router.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/searches&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_searches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM searches WHERE active = 1&lt;/span&gt;&lt;span class="sh"&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 test it. It returns your searches. Works correctly — for you, because you're the only user in development.&lt;/p&gt;

&lt;p&gt;In production, with multiple users, it returns everyone's searches to everyone. The &lt;code&gt;user_id&lt;/code&gt; filter is missing. You have a data isolation breach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this happens
&lt;/h2&gt;

&lt;p&gt;Data isolation failures are rarely intentional. They happen because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The developer tested with a single user account and didn't observe the failure&lt;/li&gt;
&lt;li&gt;The filter was present in some query functions and assumed in others&lt;/li&gt;
&lt;li&gt;A refactor removed the filter accidentally&lt;/li&gt;
&lt;li&gt;The endpoint was copied from a public-data endpoint and the filter wasn't added&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Code review catches many bugs, but data isolation failures are hard to spot unless the reviewer is specifically looking for missing &lt;code&gt;user_id&lt;/code&gt; clauses on every query.&lt;/p&gt;

&lt;h2&gt;
  
  
  The systematic audit
&lt;/h2&gt;

&lt;p&gt;Before shipping any feature, audit every endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;For each endpoint that returns data:
  □ Does the query include WHERE user_id = current_user.id?
  □ OR is this data intentionally public? (document why)
  □ OR is this an admin endpoint? (require admin role check, not just auth)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For admin bypass, the filter must be explicit, not omitted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Wrong: admin bypass by omitting the filter
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_admin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM searches&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM searches WHERE user_id = ?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# Right: explicit bypass with documentation
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_admin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Admin can see all searches for support purposes
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM searches ORDER BY created_at DESC&lt;/span&gt;&lt;span class="sh"&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 distinction matters for auditing. An omitted filter is invisible. An explicit &lt;code&gt;# Admin can see all&lt;/code&gt; comment is visible and intentional.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing negative tests
&lt;/h2&gt;

&lt;p&gt;For each scoped endpoint, write a test that verifies cross-user access is blocked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_user_cannot_see_other_users_searches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_b&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# user_b creates a search
&lt;/span&gt;    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/searches&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;keyword&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_b&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;# user_a fetches searches — should not see user_b's search
&lt;/span&gt;    &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/searches&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_a&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;slugs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;keyword&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;res&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;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;slugs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test fails immediately if the &lt;code&gt;user_id&lt;/code&gt; filter is missing. It cannot be accidentally removed during a refactor without breaking the test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Geolocation and sensitive fields: negative tests for field presence
&lt;/h2&gt;

&lt;p&gt;Some data isolation isn't about users — it's about which fields should never appear in any response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_api_response_contains_no_location_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/products&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;res&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;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;location_lat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;location_lng&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seller_address&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These negative tests verify that sensitive fields are absent from the response. They're easy to write and catch the "I added a new field to the model and forgot to exclude it from the serialiser" class of bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule
&lt;/h2&gt;

&lt;p&gt;Every query that touches user data has &lt;code&gt;user_id = current_user.id&lt;/code&gt; in its WHERE clause, or a documented, tested reason why it doesn't. There is no middle ground.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>backend</category>
    </item>
    <item>
      <title>The Stock Market Feels Like Crypto Now. Here Is What I Am Actually Doing About It.</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Fri, 22 May 2026 07:53:17 +0000</pubDate>
      <link>https://dev.to/canceylan1988/the-stock-market-feels-like-crypto-now-here-is-what-i-am-actually-doing-about-it-5aac</link>
      <guid>https://dev.to/canceylan1988/the-stock-market-feels-like-crypto-now-here-is-what-i-am-actually-doing-about-it-5aac</guid>
      <description>&lt;p&gt;I have been avoiding this article for a few weeks.&lt;/p&gt;

&lt;p&gt;Not because I do not have thoughts on it. Because after my burnout, I made a deliberate choice to stop obsessing over my portfolio. Long-term, slow, boring investing had always worked better for me than the anxious tab-switching of short-term trading. Letting it go a little felt healthy. It probably was.&lt;/p&gt;

&lt;p&gt;But Q1 numbers are in. And something is shifting in a way that is hard to ignore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters right now
&lt;/h2&gt;

&lt;p&gt;Here is the thing about Q1 2026 that does not quite add up on paper. GDP growth came in soft. Inflation stayed sticky in both the US and Europe. Consumer confidence was weak. Tariff noise from renewed escalations under the Trump administration rattled markets through January and February.&lt;/p&gt;

&lt;p&gt;And yet. Major indices hit all-time highs.&lt;/p&gt;

&lt;p&gt;That gap between the economic mood music and where markets actually landed is what I keep coming back to. Money is not sitting in broad indices waiting for macro conditions to improve. It is moving. Decisively. Into very specific corners of the market.&lt;/p&gt;

&lt;p&gt;The AI investment wave is doing something structurally similar to what crypto did to speculative appetite in 2017 and again in 2021. Except this time it is not retail traders buying tokens at 2am. It is sovereign wealth funds, pension allocators, and the largest infrastructure investors in the world redirecting capital at a speed I genuinely did not expect to see this decade.&lt;/p&gt;

&lt;p&gt;So I stopped asking "what is the market doing right now" and started asking "what are the structural shifts already in motion that will not reverse regardless of a short-term recession or rate cycle." That reframe helped me think more clearly.&lt;/p&gt;

&lt;p&gt;Here is where I landed. None of this is investment advice. These are my personal observations and the lens through which I am thinking about my own allocation. Do your own research, and talk to someone qualified before making any decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the money is actually going: three macro themes I am tracking
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Energy infrastructure
&lt;/h3&gt;

&lt;p&gt;This one feels almost unsexy compared to the AI hype. But the numbers are staggering. AI data centres and automation systems require orders of magnitude more energy than the digital infrastructure we built in the 2010s. We are not ready for that demand curve.&lt;/p&gt;

&lt;p&gt;Hydrogen, solar, wind, and grid infrastructure are seeing renewed interest from a completely different demand source than before. It is not just climate policy. It is raw computational appetite. And energy infrastructure does not go to zero when an AI model gets outcompeted. The electrons still need to flow.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The AI infrastructure stack
&lt;/h3&gt;

&lt;p&gt;Most of the market conversation is here, but there are really three distinct sub-themes worth separating.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rare earths and critical materials.&lt;/strong&gt; The hardware required to build and run AI, chips, cooling systems, sensors, storage, depends on materials with genuinely constrained supply chains. Ongoing US-China trade tensions are creating strategic urgency around sourcing. Slow burn, but the structural case is real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hardware and data centre infrastructure.&lt;/strong&gt; Chip production, rack density, power distribution, cooling. This is where the arms race is most visible. The infrastructure layer tends to be less volatile than the application layer because it services everyone, regardless of which AI model wins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A shifting top of the market.&lt;/strong&gt; OpenAI is reportedly exploring a public market path. Anthropic is a legitimate enterprise alternative with differentiated positioning. The composition of the most valuable AI companies in five years will probably look different from today, and that has real implications for where capital flows.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Robotics and autonomous systems
&lt;/h3&gt;

&lt;p&gt;The longest time horizon of the three, but the investment infrastructure is being built right now.&lt;/p&gt;

&lt;p&gt;The hardware stack is already in a full capital cycle: sensors and LiDAR, radar systems, precision localisation, connectivity infrastructure (V2X, 5G private networks), and edge compute. Tesla remains interesting because it is operating across the full stack. But the more durable plays might be component suppliers who benefit regardless of which platform wins the consumer-facing race.&lt;/p&gt;

&lt;p&gt;One thing worth watching: SpaceX IPO speculation has been circulating seriously. If and when that happens, it would be one of the largest public listings in years. Worth having a plan for in advance rather than reacting in real time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What most people get wrong
&lt;/h2&gt;

&lt;p&gt;They treat it as a short-term trade.&lt;/p&gt;

&lt;p&gt;The AI infrastructure buildout is a multi-decade capital cycle. The companies that win in 2026 are not necessarily the ones that win in 2032. If you are buying into these themes expecting a clean 18-month return, you will probably sell at the wrong moment.&lt;/p&gt;

&lt;p&gt;A genuine risk worth naming: a short-term recession or credit event is still entirely possible. The macro picture in Q1 2026 is genuinely uncertain, tariff volatility is not resolved, and consumer balance sheets in the US are more stretched than the headline numbers suggest. A long-term structural thesis does not immunise you from a painful drawdown along the way.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When the market starts behaving like crypto, the question is not whether to panic. It is whether your allocation was built for a world that no longer exists.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Audit your current holdings for real exposure.&lt;/strong&gt; Pull up the top 10 underlying positions in any fund you hold. You may be less diversified than the fund name implies. Adjust from the actual holdings, not the label.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Think in layers, not bets.&lt;/strong&gt; Energy infrastructure, AI hardware, and robotics components carry three different risk and time profiles. Size them accordingly. Infrastructure tends to be more stable. Growth stocks and application-layer plays carry more variance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep a portion boring on purpose.&lt;/strong&gt; The reallocation I am describing is not a wholesale pivot. It is a considered addition to a base that stays slow and steady. Do not let macro excitement push you into over-rotation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch for liquidity events.&lt;/strong&gt; A potential OpenAI IPO, a potential SpaceX listing, and continued rare earth ETF development in European markets are moments worth planning for in advance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not mistake volatility for opportunity without a thesis.&lt;/strong&gt; Some growth stock charts right now look extraordinary. That is not a reason to buy. The question is whether you understand why they are moving and whether that reason holds over the period you intend to hold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Talk to someone qualified before acting on any of this.&lt;/strong&gt; Genuinely. I am thinking out loud about my own allocation, not giving you a roadmap for yours.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Are there themes I missed? Probably. Biotech, longevity tech, and defence and aerospace are all conversations worth having in a future piece.&lt;/p&gt;

&lt;p&gt;For now: long on structural disruption, honest about short-term risk, and no longer pretending that the allocation I built in a different macro environment is still the right one.&lt;/p&gt;

</description>
      <category>financeinvesting</category>
    </item>
    <item>
      <title>Lazy backfill: roll out new data shapes without a migration script</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Mon, 18 May 2026 04:24:07 +0000</pubDate>
      <link>https://dev.to/canceylan1988/lazy-backfill-roll-out-new-data-shapes-without-a-migration-script-16g5</link>
      <guid>https://dev.to/canceylan1988/lazy-backfill-roll-out-new-data-shapes-without-a-migration-script-16g5</guid>
      <description>&lt;h2&gt;
  
  
  The instinct: write a migration script
&lt;/h2&gt;

&lt;p&gt;You add a new field to a stored record type. Existing records don't have it. The straightforward fix is a migration script: fetch every record, add the field, write it back.&lt;/p&gt;

&lt;p&gt;Migration scripts are fine for relational databases with schema enforcement. For document stores, KV stores, and any system where records are read more often than they are written, they create unnecessary operational risk:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The migration runs once, at a moment you choose, and you have to be present for it&lt;/li&gt;
&lt;li&gt;If it fails halfway through, you have partial state&lt;/li&gt;
&lt;li&gt;You need to coordinate the migration with the deployment of the code that expects the new field&lt;/li&gt;
&lt;li&gt;For large record sets, the migration may time out or hit rate limits&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The alternative: normalize on read
&lt;/h2&gt;

&lt;p&gt;Instead of migrating upfront, detect missing fields on every read and fill them lazily.&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalizeRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Article&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;Article&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// New field: distributionState — missing on older records&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;distributionState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;distributionState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;distributionState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;distributionState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildDistributionState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;article&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;distributionMap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getDistributionMap&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;socialPosts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;socialPosts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// New field: publishedAt — recoverable from legacy signals&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;publishedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;publishedAt&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;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;published&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;publishedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;publishedAt&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;inferLegacyPublishedAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&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;changed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;distributionState&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;distributionState&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;publishedAt&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;publishedAt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Only write if something actually changed — prevents infinite write loops&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;changed&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;record&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;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;distributionState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;publishedAt&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;kv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;recordKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;updated&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;updated&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 pattern works in three phases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;First read:&lt;/strong&gt; the field is missing, so it gets computed and written back. One KV write.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every subsequent read:&lt;/strong&gt; the field is present. The &lt;code&gt;!changed&lt;/code&gt; guard returns early. Zero extra writes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollout complete:&lt;/strong&gt; after every record has been read at least once, all records are normalized. No migration script was needed.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The changed-guard is critical
&lt;/h2&gt;

&lt;p&gt;Without the &lt;code&gt;changed&lt;/code&gt; check, the normalize function writes on every read — even when nothing changed. This turns every GET into a GET + SET, multiplying write load and potentially triggering unnecessary index updates.&lt;/p&gt;

&lt;p&gt;Use reference equality (&lt;code&gt;!==&lt;/code&gt;) for object fields: if the field was already present and you didn't build a new object, the reference is unchanged, and &lt;code&gt;changed&lt;/code&gt; stays false.&lt;/p&gt;

&lt;p&gt;For primitive fields (strings, booleans), compare values directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use this pattern
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Good fit:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adding optional or derivable fields to stored records&lt;/li&gt;
&lt;li&gt;Fields that can be inferred from other existing data (timestamps from logs, structured state from legacy flat data)&lt;/li&gt;
&lt;li&gt;Systems where reads are frequent and records are accessed regularly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Not a good fit:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fields that are required immediately at write time and cannot be inferred from existing data&lt;/li&gt;
&lt;li&gt;Schema changes that alter how existing fields are interpreted (requires explicit migration)&lt;/li&gt;
&lt;li&gt;Relational databases with foreign key constraints&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The "what not to rewrite" rule
&lt;/h2&gt;

&lt;p&gt;Lazy backfill can introduce a subtle bug: if the derivation logic changes after some records have already been normalized, early-normalized records will have the old shape while un-normalized records will get the new shape.&lt;/p&gt;

&lt;p&gt;The fix: only backfill when the field is completely absent. Never rewrite an existing field just because the derivation logic changed. If the logic needs to change for existing records, that is a deliberate migration decision, not a lazy normalization.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✓ Only backfill when missing&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;distributionState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;distributionState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildDistributionState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ✗ Don't rewrite existing state just because the build logic changed&lt;/span&gt;
&lt;span class="c1"&gt;// record.distributionState = buildDistributionState(record); // always overwrites&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This rule ensures the backfill is idempotent and safe to run in production without supervision.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Test what the toast cannot prove</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Mon, 18 May 2026 04:23:42 +0000</pubDate>
      <link>https://dev.to/canceylan1988/test-what-the-toast-cannot-prove-3jh1</link>
      <guid>https://dev.to/canceylan1988/test-what-the-toast-cannot-prove-3jh1</guid>
      <description>&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;An event form looked healthy from the outside. The user changed fields, clicked save, and saw a success toast.&lt;/p&gt;

&lt;p&gt;But the saved data was not trustworthy. Address changes could keep the previous map coordinates. The edit form accepted weak input. The create form had validation state that could drift from the data being submitted. The test suite did not catch it because it mostly checked that the dialog closed or that a success screen appeared.&lt;/p&gt;

&lt;p&gt;That is the dangerous part: the UI told the truth about the request finishing, not about the data being correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root cause
&lt;/h2&gt;

&lt;p&gt;The tests were proving the wrong contract.&lt;/p&gt;

&lt;p&gt;They asserted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the form could be filled&lt;/li&gt;
&lt;li&gt;the save button could be clicked&lt;/li&gt;
&lt;li&gt;the success state appeared&lt;/li&gt;
&lt;li&gt;the dialog closed&lt;/li&gt;
&lt;li&gt;the updated title was visible somewhere on the page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are useful smoke checks, but they are not persistence checks.&lt;/p&gt;

&lt;p&gt;For a real create/edit flow, the contract is bigger:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the browser selected the intended autocomplete result&lt;/li&gt;
&lt;li&gt;the payload included the derived fields&lt;/li&gt;
&lt;li&gt;the server accepted only valid input&lt;/li&gt;
&lt;li&gt;the database stored the intended values&lt;/li&gt;
&lt;li&gt;the next read returns those values&lt;/li&gt;
&lt;li&gt;stale client cache did not hide a failed save&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the E2E test stops at the toast, a broken save path can still pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it was non-obvious
&lt;/h2&gt;

&lt;p&gt;Forms often have derived state.&lt;/p&gt;

&lt;p&gt;An address field might populate city, postal code, region, latitude, and longitude. A date field might generate opening-hour rows. A login dialog might resume a draft submit. An edit form might start with existing coordinates, then accidentally keep them after the address text changes.&lt;/p&gt;

&lt;p&gt;The visible input is only one layer. The meaningful saved record is the combination of typed fields, derived fields, validation, API behavior, and refetch behavior.&lt;/p&gt;

&lt;p&gt;That is why "I saw the new text on the card" can be misleading. The card may be optimistic, partially refreshed, or showing only one field while the broken field remains hidden.&lt;/p&gt;

&lt;h2&gt;
  
  
  The better E2E rule
&lt;/h2&gt;

&lt;p&gt;For important forms, every save test should have a readback assertion.&lt;/p&gt;

&lt;p&gt;After create:&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;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/create/i&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&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="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;heading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/created/i&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/items/mine&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;records&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;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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;saved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;uniqueName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Selected Street 1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeCloseTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;48.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;openingHours&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;09:00-18:00&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;After edit:&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;await&lt;/span&gt; &lt;span class="nx"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/save/i&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/items/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;saved&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;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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;updatedName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;updatedAddress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeCloseTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expectedLongitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important move is not the exact API path. It is the discipline: test the persisted record after the UI says success.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make external services deterministic
&lt;/h2&gt;

&lt;p&gt;Autocomplete and geocoding are classic sources of flaky tests. Do not depend on a live map provider in a form-save regression test.&lt;/p&gt;

&lt;p&gt;Mock the search endpoint with a known result:&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;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;**/api/geocode/search?**&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fulfill&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="na"&gt;display_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Selected Street 1, City&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;48.2000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;lon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;16.3000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;road&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Selected Street&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;house_number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;City&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the test verifies your app logic: dropdown selection, derived fields, payload, persistence, and readback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reusable rule
&lt;/h2&gt;

&lt;p&gt;A toast proves that code reached a happy branch. It does not prove that the right data survived the round trip.&lt;/p&gt;

&lt;p&gt;For any create/edit form with derived fields, cache, or autocomplete, write at least one E2E test that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;uses deterministic external-service mocks&lt;/li&gt;
&lt;li&gt;submits through the real UI&lt;/li&gt;
&lt;li&gt;reads back through the API&lt;/li&gt;
&lt;li&gt;asserts the fields users cannot easily see&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hidden fields are where save bugs like to live.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>testing</category>
    </item>
    <item>
      <title>Swipe Right on This: Dating Apps and LLMs Are Running the Same Playbook</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Thu, 14 May 2026 17:29:42 +0000</pubDate>
      <link>https://dev.to/canceylan1988/swipe-right-on-this-dating-apps-and-llms-are-running-the-same-playbook-2al0</link>
      <guid>https://dev.to/canceylan1988/swipe-right-on-this-dating-apps-and-llms-are-running-the-same-playbook-2al0</guid>
      <description>&lt;h1&gt;
  
  
  Swipe Right on This: Dating Apps and LLMs Are Running the Same Playbook
&lt;/h1&gt;

&lt;p&gt;Somewhere between my third Claude session and my seventh Tinder swipe, I noticed something strange. Both had cut me off.&lt;/p&gt;

&lt;p&gt;Not in a dramatic way. Just that quiet wall. "You've reached your limit." The digital equivalent of a bouncer pointing at the velvet rope.&lt;/p&gt;

&lt;h2&gt;
  
  
  The surface similarity is almost too obvious
&lt;/h2&gt;

&lt;p&gt;Dating apps and LLMs both gate their core functionality behind usage limits. Hourly caps, daily caps, weekly caps. You can do the thing, just not too much of the thing. And if you want more, you can pay for it.&lt;/p&gt;

&lt;p&gt;The root causes are completely different, which is what makes the parallel interesting rather than trivial.&lt;/p&gt;

&lt;p&gt;For LLMs, the limit is mostly honest. Inference is expensive. Running GPT-4 or Claude Opus at scale costs real money per token, and until the supply side catches up with demand, platforms have to ration access or go broke. The limit is infrastructure. It is temporary, at least in theory.&lt;/p&gt;

&lt;p&gt;For dating apps, the limit is a business model. Tinder is not rationing swipes because their servers are struggling. They are rationing swipes because scarcity is the product. You feel the limit. You feel the pull to remove it. That tension is the conversion funnel.&lt;/p&gt;

&lt;p&gt;Two completely different problems. Identical UX solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the limit might actually be doing you a favour
&lt;/h2&gt;

&lt;p&gt;Here is the uncomfortable part: the limit is probably good for you, even when the motivation behind it is purely commercial.&lt;/p&gt;

&lt;p&gt;Imagine a version of Tinder with no constraints. Unlimited swipes, no cooldown, no friction. For most people, that would not feel like freedom. It would feel like a slot machine with an infinite pull lever and no payout ceiling. The dopamine loop would eat the rest of your evening and a portion of your self-esteem.&lt;/p&gt;

&lt;p&gt;The limit forces something like intention. You get fifty swipes, so you start reading profiles. You get three free Claude messages on the heavy model, so you think before you type.&lt;/p&gt;

&lt;p&gt;Scarcity creates attention. Which is maybe why meditation teachers have been saying it for centuries, just without the app store rating.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The limit is not there to protect the platform. It is there to protect you from yourself, and then charge you when protection stops working.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The algo paradox nobody talks about
&lt;/h2&gt;

&lt;p&gt;Here is where it gets genuinely strange. Dating apps penalise overuse.&lt;/p&gt;

&lt;p&gt;Swipe too fast, too indiscriminately, and the algorithm quietly deprioritises your profile. Your reach drops. Your matches slow down. The platform is simultaneously selling you unlimited swipes and punishing you for using them.&lt;/p&gt;

&lt;p&gt;That is a product contradiction so strange it almost sounds made up. The commercial incentive says "engage more, pay more." The algorithmic incentive says "slow down or we will slow you down."&lt;/p&gt;

&lt;p&gt;I would genuinely love to spend a few weeks inside a Match Group product meeting to understand how they hold that tension. It must be a fascinating conversation. Or a deeply uncomfortable one.&lt;/p&gt;

&lt;p&gt;The LLM side does not have this paradox yet, because the limits are supply-side, not behavioural. But the question worth sitting with is: what happens when AI supply overtakes demand? When inference gets cheap enough that limits become optional rather than necessary?&lt;/p&gt;

&lt;p&gt;Will Anthropic and OpenAI discover what Tinder already knows? That the limit was never really about the infrastructure. That the moment before the paywall is the most valuable real estate in the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gender statistics nobody wants to say out loud
&lt;/h2&gt;

&lt;p&gt;While we are here, there is a structural problem with dating apps that LLMs might actually help fix, and it is bigger than anyone in the industry wants to advertise.&lt;/p&gt;

&lt;p&gt;The gender ratio on most major dating platforms sits somewhere around 70 to 30, men to women. Some platforms skew even further. That demographic imbalance creates a market dynamic that has nothing to do with romance and everything to do with supply and demand. Men compete furiously for attention. Women filter from abundance. Neither situation is particularly healthy for either side, but the apps profit from the friction regardless.&lt;/p&gt;

&lt;p&gt;It is less like dating and more like an ecosystem where one species massively outnumbers the other. The Serengeti, but with push notifications.&lt;/p&gt;

&lt;p&gt;What I find genuinely hopeful is the current wave of vibe-coded, LLM-assisted apps being built by people outside the traditional VC-backed dating industry. When the barrier to building a dating product drops low enough, someone will eventually build one with different incentive structures. Fairer matching mechanics. Less weaponised scarcity. Behavioural science used to create connection rather than to extract subscription revenue. Maybe I'll tackle that one day, my ADHD Brain wants to start it right away, however I have to focus on my current focus. Creating a flea market ecosystem that is better than the current ones. More on that, hopefully soon.&lt;/p&gt;

&lt;p&gt;I do not know what that could look like exactly. But I would bet (or I hope) it gets built in the next three years by someone using AI tools to move fast and a social science background to think clearly. I am watching this space, and I will keep you posted. Likewise, if you need help on doing that, hit me up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd actually do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Treat usage limits as useful friction, not just annoyance.&lt;/strong&gt; When an LLM or app cuts you off, use the pause to ask whether you were being intentional or just scrolling on autopilot. The limit is information.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learn the algorithm before you try to beat it.&lt;/strong&gt; On dating apps, quality signals matter more than volume. On LLMs, prompt quality matters more than prompt frequency. The platforms reward users who engage with intention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you are building anything in the dating or social app space, look at what LLM tools have made newly possible.&lt;/strong&gt; The infrastructure cost of launching a niche dating product has dropped dramatically. The interesting white space is in underserved communities and fairer matching models, not in replicating Tinder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch what happens to LLM pricing over the next two years.&lt;/strong&gt; If compute costs drop fast enough, the current usage limits will become a choice rather than a necessity. How OpenAI and Anthropic handle that moment will tell you a lot about whether they think like infrastructure companies or like consumer apps. This is my personal read on where things are heading, not financial advice of any kind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;And if you are on a dating app right now, this is your sign to slow down.&lt;/strong&gt; Not because the algorithm will punish you if you do not, though it might. But because the best thing about the limit is that it gives you a moment to ask whether you actually want what you are chasing, or whether you have just been conditioned to chase it. That question is worth more than another fifty swipes.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>techai</category>
    </item>
    <item>
      <title>Layering data sources: accept both APIs as fallback, don't choose one</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Tue, 12 May 2026 12:36:49 +0000</pubDate>
      <link>https://dev.to/canceylan1988/layering-data-sources-accept-both-apis-as-fallback-dont-choose-one-1kn2</link>
      <guid>https://dev.to/canceylan1988/layering-data-sources-accept-both-apis-as-fallback-dont-choose-one-1kn2</guid>
      <description>&lt;h2&gt;
  
  
  The single-source problem
&lt;/h2&gt;

&lt;p&gt;You pick one free data API for financial information. It works well most of the time. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some companies report through non-standard channels and the API misses them&lt;/li&gt;
&lt;li&gt;Cash flow data for certain sectors is systematically wrong&lt;/li&gt;
&lt;li&gt;The API rate-limits you and returns empty data without saying so&lt;/li&gt;
&lt;li&gt;A company restructuring causes a gap in the data for 2–3 weeks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your analysis breaks silently for affected companies. You don't know which ones until you manually check.&lt;/p&gt;

&lt;h2&gt;
  
  
  The layering pattern
&lt;/h2&gt;

&lt;p&gt;Instead of choosing one source, define a priority order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_cash_flow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Try the primary source
&lt;/span&gt;    &lt;span class="n"&gt;primary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_from_primary_api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;freeCashFlow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;primary&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;primary&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Fall back to secondary source (e.g. official regulatory filings)
&lt;/span&gt;    &lt;span class="n"&gt;secondary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_from_sec_filings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FreeCashFlow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;secondary&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;secondary&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. No data available
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The primary source handles the majority of cases. The secondary source catches the gaps. Neither source needs to be perfect — together they cover more of the space.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "fallback" is better than "merge"
&lt;/h2&gt;

&lt;p&gt;A tempting alternative is to merge data from both sources — average them, or take the max, or reconcile differences. This is more complex and introduces new failure modes: what if the two sources disagree significantly? Which one is right?&lt;/p&gt;

&lt;p&gt;The fallback pattern is simpler: primary is trusted if available; secondary is used only when primary is absent. You never have to reconcile disagreement because you never look at the secondary if the primary gave you something.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate limit isolation
&lt;/h2&gt;

&lt;p&gt;Two sources also means two rate limit buckets. If the primary API rate-limits you, the secondary is unaffected. You can fetch from the secondary while the primary recovers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ticker&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tickers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_data_with_fallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# still rate-limit between requests
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sleep still applies — you're still making requests to external APIs. But the sleep now protects two APIs simultaneously, and a rate limit on one doesn't stop the pipeline entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logging which source was used
&lt;/h2&gt;

&lt;p&gt;For debugging and data quality monitoring, log which source provided each data point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DataPoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;  &lt;span class="c1"&gt;# "primary", "secondary", "none"
&lt;/span&gt;    &lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets you answer: "what percentage of our data is coming from the fallback?" A high fallback rate for a specific metric signals that the primary source has a systematic gap there.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to add a third source
&lt;/h2&gt;

&lt;p&gt;Two sources cover most gaps. Add a third only when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have a specific metric that both primary and secondary miss for a meaningful portion of your universe&lt;/li&gt;
&lt;li&gt;The third source requires significantly different authentication or rate limiting&lt;/li&gt;
&lt;li&gt;You've measured the gap and it materially affects your analysis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't add sources speculatively. Each additional source adds maintenance overhead and the possibility of new failure modes. Add them in response to measured gaps, not anticipated ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  The principle
&lt;/h2&gt;

&lt;p&gt;Resilience in data pipelines comes from redundancy, not from finding the perfect single source. Accept that any single free API will have gaps. Layer sources to fill the gaps, log which source filled each gap, and monitor the distribution over time.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>backend</category>
    </item>
    <item>
      <title>When the bug isn't a bug: diagnosing runtime barriers before debugging</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Tue, 12 May 2026 12:36:42 +0000</pubDate>
      <link>https://dev.to/canceylan1988/when-the-bug-isnt-a-bug-diagnosing-runtime-barriers-before-debugging-2781</link>
      <guid>https://dev.to/canceylan1988/when-the-bug-isnt-a-bug-diagnosing-runtime-barriers-before-debugging-2781</guid>
      <description>&lt;h2&gt;
  
  
  The pattern that wastes days
&lt;/h2&gt;

&lt;p&gt;You need a capability — web scraping, image processing, ML inference. You reach for your existing stack. You try library A, it fails. You try library B, same class of error. You try a workaround, it partially works. You try another workaround. Three days later you have a brittle solution held together with patches.&lt;/p&gt;

&lt;p&gt;The diagnosis that would have saved those three days: the runtime is wrong for this capability. No amount of library-switching or workaround-stacking will produce a clean solution, because the underlying problem is structural.&lt;/p&gt;

&lt;p&gt;This is a &lt;strong&gt;runtime barrier&lt;/strong&gt; — a mismatch between what your current environment can do well and what you're asking it to do.&lt;/p&gt;

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

&lt;p&gt;A runtime barrier looks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The same error class persists across multiple different libraries&lt;/li&gt;
&lt;li&gt;Workarounds work partially but introduce new problems&lt;/li&gt;
&lt;li&gt;The error occurs at a level below your code (TLS, native module, OS)&lt;/li&gt;
&lt;li&gt;The ecosystem for this capability is thin or unmaintained in your language&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Common examples:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Poor-fit runtime&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Web scraping with anti-bot&lt;/td&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;403s or empty results that Python handles fine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ML inference&lt;/td&gt;
&lt;td&gt;Node.js / Go&lt;/td&gt;
&lt;td&gt;No native tensor runtime; everything is a wrapper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heavy parallel computation&lt;/td&gt;
&lt;td&gt;Python (GIL)&lt;/td&gt;
&lt;td&gt;CPU-bound tasks don't parallelise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reactive UI&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;No native component model; everything is a workaround&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The diagnosis process
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Name the capability precisely.&lt;/strong&gt;&lt;br&gt;
Not "it's not working" — "I'm trying to make authenticated HTTP requests that bypass bot detection." Precise naming lets you assess fit against known ecosystem strengths.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Check the ecosystem.&lt;/strong&gt;&lt;br&gt;
Search for the 3 most popular libraries for this capability in your runtime. If they all have the same class of failure or are unmaintained, that's an ecosystem gap, not a library bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Cross-reference with a reference runtime.&lt;/strong&gt;&lt;br&gt;
Does the capability work cleanly in another language? If Python's &lt;code&gt;requests&lt;/code&gt; + anti-bot library handles this in 10 lines, and Node.js has no equivalent after 3 library attempts, the gap is real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: State the barrier clearly.&lt;/strong&gt;&lt;br&gt;
"This is a runtime barrier. Node.js is the wrong tool for anti-bot scraping. No amount of debugging will fix this — the ecosystem gap is fundamental."&lt;/p&gt;

&lt;p&gt;This is a hard sentence to say, especially after investment in a particular approach. It's also the sentence that unblocks progress.&lt;/p&gt;
&lt;h2&gt;
  
  
  The architectural response
&lt;/h2&gt;

&lt;p&gt;Once you've diagnosed a barrier, the solution is a service boundary — not a workaround.&lt;/p&gt;

&lt;p&gt;A service boundary means: let each capability live in the runtime best suited to it, and define a clean interface between them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A — separate process:&lt;/strong&gt; The capability runs as a standalone process in the right runtime. It writes results to a shared database or communicates via HTTP. Your main application reads from the database. No shared runtime, no compromise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option B — purpose-built script:&lt;/strong&gt; For scheduled or batch work, a standalone script in the right language is called by your scheduler. It doesn't live in your main application at all.&lt;/p&gt;

&lt;p&gt;What you don't do: embed the capability as a subprocess call (&lt;code&gt;python script.py&lt;/code&gt; from Node.js, &lt;code&gt;exec()&lt;/code&gt;, shell-out). This creates two runtimes with two package managers, two test runners, and deployment confusion. It looks like a solution and is actually a maintenance problem.&lt;/p&gt;
&lt;h2&gt;
  
  
  Document the barrier
&lt;/h2&gt;

&lt;p&gt;Once a runtime barrier is diagnosed and resolved, document it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Runtime Barrier — [date]&lt;/span&gt;

&lt;span class="gs"&gt;**Capability:**&lt;/span&gt; Anti-bot web scraping
&lt;span class="gs"&gt;**Runtime attempted:**&lt;/span&gt; Node.js
&lt;span class="gs"&gt;**Failure:**&lt;/span&gt; All tested libraries produced empty results against DataDome protection
&lt;span class="gs"&gt;**Resolution:**&lt;/span&gt; Python scraper process writes to shared SQLite; Node.js reads from it
&lt;span class="gs"&gt;**Do not re-attempt:**&lt;/span&gt; Node.js scraping for this target — the barrier is structural
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This entry prevents a future developer (or a future version of yourself) from re-attempting the same failed approach and re-discovering the same barrier from scratch.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>architecture</category>
    </item>
    <item>
      <title>ChatGPT Has a Human Problem. And That's Not a Compliment.</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Mon, 11 May 2026 17:43:53 +0000</pubDate>
      <link>https://dev.to/canceylan1988/chatgpt-has-a-human-problem-and-thats-not-a-compliment-45c3</link>
      <guid>https://dev.to/canceylan1988/chatgpt-has-a-human-problem-and-thats-not-a-compliment-45c3</guid>
      <description>&lt;p&gt;It started with a football question.&lt;/p&gt;

&lt;p&gt;I asked ChatGPT about the training camp situation at Real Madrid, which is all over my social media feeds. The response came back detailed, confident, and most and foremost completely wrong. It told me Xabi Alonso was managing internal tensions in the squad — navigating player dynamics, shaping the dressing room. The only problem: Alonso had left the club months earlier. When I pushed back, the model corrected itself immediately. Right answer the second time. But only because I knew enough to challenge it.&lt;/p&gt;

&lt;p&gt;Most people don't challenge it. They read the first answer, trust the confident framing, and move on.&lt;/p&gt;

&lt;p&gt;That moment stuck with me. Not because it was catastrophic, it wasn't, but because it was a perfect, low-stakes illustration of something that matters a great deal when the stakes are higher.&lt;/p&gt;




&lt;p&gt;I use ChatGPT as a sparring partner. Not a search engine, not a coding autocomplete, a genuine second opinion when I'm working through an idea. Most of the time, it earns that role. Then, sometimes, it reminds me why I shouldn't fully trust it.&lt;/p&gt;

&lt;p&gt;Two things have been bothering me for a while. I've been sitting on this because I didn't want to write a generic "AI has limits" piece, those exist in abundance and mostly say nothing. But this cuts deeper than a product complaint. It touches on something that actually matters: whether tools like ChatGPT are narrowing the gap between institutional intelligence and everyday people, or quietly widening it in ways we don't immediately notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;There's a version of AI that genuinely democratises access to quality thinking. A private investor using ChatGPT to stress-test a thesis could, theoretically, operate with a level of analytical rigour that was previously reserved for people with Bloomberg terminals and research teams. A small founder could pressure-test a go-to-market strategy the way a consultant would. That potential is real. I've seen it work.&lt;/p&gt;

&lt;p&gt;But two recurring failure modes are eating into that promise and they're not random bugs. They're structural, and they mirror some of the most well-documented cognitive errors in human decision-making. Which makes them more dangerous, not less, because they feel like intelligence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why ChatGPT gets recent events wrong
&lt;/h2&gt;

&lt;p&gt;The training data issue is the more obvious of the two, but it keeps surprising people. ChatGPT, even the paid versions, works from a training cutoff. After that date, it doesn't know what happened. It wasn't there.&lt;/p&gt;

&lt;p&gt;The problem isn't the cutoff itself. That's a technical constraint and it's disclosed. The problem is that the model doesn't always flag its own blind spot. It answers with the same confident tone whether it's recalling something it was trained on thoroughly or reconstructing something it barely has data on.&lt;/p&gt;

&lt;p&gt;Which brings me back to Real Madrid. The model didn't say "I'm not sure, my information might be outdated." It just told me about Xabi Alonso managing the squad like it was current fact. The tone was the same as it would be for anything else. No signal that it was working from stale data. No asterisk.&lt;/p&gt;

&lt;p&gt;That's the structural problem. In a world where people are increasingly using AI to verify things they see on social media, a model that delivers guesses with the same confidence as facts is a meaningful failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  ChatGPT recency bias in investing: why this is genuinely risky
&lt;/h2&gt;

&lt;p&gt;The second issue is subtler and, to me, more concerning: recency bias.&lt;/p&gt;

&lt;p&gt;Recency bias is a well-documented human cognitive pattern where we overweight recent events and assume current trends will continue. It's why investors pile into assets after a run-up and exit after a drawdown — the opposite of what the math suggests. Good analysts are trained to fight it. It's one of the reasons systematic, rules-based investing tends to outperform discretionary judgment over long horizons.&lt;/p&gt;

&lt;p&gt;ChatGPT has this bias. And it has it in a specific, almost ironic way: because its training data is skewed toward what was written about recently, the model reflects whatever narrative was dominating the news cycle at the time of training. If the last six months of its data were full of coverage about a rate hike cycle, it will anchor to that. If a particular sector was getting hyped in the financial press, the model will subtly overweight that framing.&lt;/p&gt;

&lt;p&gt;Ask it for a view on a macro environment or a company and it often does one of two things: it either extrapolates from the most recent narrative it has data on, or it hedges so aggressively that the answer is useless. Neither is what a good analyst would do.&lt;/p&gt;

&lt;p&gt;This matters for private investors in particular. (Nothing here is financial advice, just thinking out loud about the tool itself, and you should absolutely form your own view before acting on anything.) The gap between institutional and retail investors has always partly been about access to dispassionate, systematic analysis. If AI is going to help close that gap, it needs to be the thing that resists headline-driven framing, not the thing that encodes it at scale. Right now, in my experience, it's closer to the latter.&lt;/p&gt;

&lt;p&gt;I know you can build systems around this. Custom instructions, structured prompts, injecting your own context and constraints. I do some of this. But I shouldn't have to spend two hours engineering guardrails just to get a second opinion that isn't contaminated by whatever was trending six months ago. That's not a sparring partner. That's a liability dressed up as one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What most people get wrong about AI reliability
&lt;/h2&gt;

&lt;p&gt;The common response to both of these issues is: "Just fact-check it." And yes, obviously. But that misses the point.&lt;/p&gt;

&lt;p&gt;The value of a tool like ChatGPT isn't just that it gives you information. It's that it gives you information at a speed and scale that changes how you think and work. The moment you have to manually verify every output, you've lost a big part of that value. And the model's confident delivery actively works against your instinct to check, it doesn't sound uncertain, so you don't treat it as uncertain.&lt;/p&gt;

&lt;p&gt;We engineered something close to a superpower and then quietly installed the same cognitive bugs that make humans terrible at making decisions under pressure.&lt;/p&gt;

&lt;p&gt;The irony is that these aren't hard problems in principle. Recency bias could be countered by training architectures that deliberately weight longer time horizons. Outdated information could be flagged more aggressively when the model's confidence should be low. Real-time retrieval exists and is improving. I've seen better behaviour in some of the newer browsing-enabled configurations, and I'll keep testing as these evolve. But the default experience still fails in the ways I've described often for the users who need it most: people who aren't prompting systematically and aren't in a position to know when the model is working from stale data.&lt;/p&gt;

&lt;p&gt;That's the gap worth worrying about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger race: whoever translates human will into product wins
&lt;/h2&gt;

&lt;p&gt;Here's my actual long-term belief about where this is all going.&lt;/p&gt;

&lt;p&gt;The LLM that most perfectly translates human will into product will win the race. Not the one with the most parameters. Not the one with the flashiest benchmark. The one that takes what you mean and turns it into what you need — at the speed of thought, without friction.&lt;/p&gt;

&lt;p&gt;This is why vibe coding and Claude Code matter so much. Not just as features, but as a signal. Claude Code doesn't ask you to adapt to its syntax. It listens to what you're trying to build and builds it. It translates intent into product in a way that, even a year ago, felt like science fiction. That's a genuinely revolutionary step — and it's brought Anthropic much, much closer to OpenAI than most people expected. I think it will be remembered as the moment the race changed shape. Not "who has the best model" but "who best understands what the human actually wants."&lt;/p&gt;

&lt;p&gt;ChatGPT's failure modes I've described, the stale confidence, the recency bias, are, at their core, failures of translation. The model produces something that sounds like what you wanted, but isn't actually responsive to what you needed. It's fluent without being useful. And in a world where the bar is shifting toward perfect intent-to-output translation, fluency alone won't be enough.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Treat ChatGPT's confident tone as neutral, not as a signal of accuracy.&lt;/strong&gt; If the topic is time-sensitive (markets, current events, recent signings), assume the information could be six to twelve months behind and verify before acting on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask the model to declare its uncertainty.&lt;/strong&gt; Something like: "Before you answer, tell me how confident you are and whether this could be affected by outdated training data." It won't always catch it, but it changes the dynamic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For anything financial, use it for frameworks, not for facts.&lt;/strong&gt; It's good at helping you think through a thesis structure, stress-test assumptions, or identify what you might be missing. It's not reliable for current valuations, recent earnings, or macro data. (And again that's my approach, not a recommendation for yours.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When something sounds confidently wrong, push back immediately.&lt;/strong&gt; The model updates. But you have to be the one who knows enough to push. Which means you still need independent knowledge, AI doesn't replace that, it extends it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build in a "what year does this assume" check.&lt;/strong&gt; Especially for anything involving companies, geopolitics, sports, or regulatory environments. If the model's answer only makes sense in a world from a year ago, it probably is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch this space.&lt;/strong&gt; Real-time retrieval and better uncertainty signalling are genuinely improving — I plan to write more about how the tooling is evolving as I test it. But the default product today still has these gaps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The potential is real. So is the gap between the potential and what ships by default. Knowing the difference is most of the work.&lt;/p&gt;




&lt;p&gt;And just to close the loop on the Real Madrid rumours: I went back and asked a more specific follow-up about the rumoured squad inner conflicts. The model confidently weighed in on Valverde's leadership role and his conflict in training with Tchouaméni and cited some local newspapers.&lt;/p&gt;

&lt;p&gt;I mean... Valverde. Come on. I know you're the captain and everything, but seriously challenging Tchouaméni? That guy is a beast, but much respect for trying...&lt;/p&gt;

</description>
      <category>techai</category>
    </item>
    <item>
      <title>Batch email sends before rate limits look like caps</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Mon, 11 May 2026 09:01:36 +0000</pubDate>
      <link>https://dev.to/canceylan1988/batch-email-sends-before-rate-limits-look-like-caps-5gcf</link>
      <guid>https://dev.to/canceylan1988/batch-email-sends-before-rate-limits-look-like-caps-5gcf</guid>
      <description>&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;A small newsletter campaign was sent to 13 subscribers.&lt;/p&gt;

&lt;p&gt;The result came back in a strangely neat shape: 5 accepted, 8 failed.&lt;/p&gt;

&lt;p&gt;At first glance, that looks like a hidden provider cap. Maybe the free tier only allows five recipients. Maybe the campaign endpoint has a quiet limit. Maybe something is wrong with the subscriber list after the first few addresses.&lt;/p&gt;

&lt;p&gt;The number felt meaningful, and it was. Just not in the way it first appeared.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root cause
&lt;/h2&gt;

&lt;p&gt;The code was batching conceptually, but not at the HTTP request level.&lt;/p&gt;

&lt;p&gt;It sliced subscribers into a "batch" and then used &lt;code&gt;Promise.all()&lt;/code&gt; to send one request per recipient inside that batch. So 13 subscribers still became 13 simultaneous API calls.&lt;/p&gt;

&lt;p&gt;The email provider had a default request-rate limit. The first few requests were accepted, and the rest crossed the throttle. The UI then surfaced the result as "5 sent, 8 failed", which made the failure look like a recipient limit instead of a request-rate problem.&lt;/p&gt;

&lt;p&gt;That is the trap: a loop named "batch" is not the same thing as using a batch API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it was non-obvious
&lt;/h2&gt;

&lt;p&gt;The failure did not look like a classic rate-limit bug.&lt;/p&gt;

&lt;p&gt;There was no slow ramp-up, no obvious retry storm, and no broken email template. The campaign worked for test sends. It worked for small recipient counts. It failed only when the list was large enough to cross the provider's request ceiling in a single burst.&lt;/p&gt;

&lt;p&gt;The misleading part was that "five successful sends" looked like a business rule.&lt;/p&gt;

&lt;p&gt;But the system was not limited to five recipients. It was limited to roughly five requests in the same small time window.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Use the provider's real batch-send endpoint.&lt;/p&gt;

&lt;p&gt;Instead of making one API request per subscriber, build one payload containing many recipient-specific messages and submit it as a single batch request. Keep the per-recipient details where they matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;individual unsubscribe URLs&lt;/li&gt;
&lt;li&gt;recipient-specific headers or metadata&lt;/li&gt;
&lt;li&gt;per-message errors returned by the provider&lt;/li&gt;
&lt;li&gt;a clear count of accepted and failed deliveries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For small newsletters, that can turn 13 simultaneous requests into one request. For larger lists, chunk into the provider's documented batch size and send those chunks deliberately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reusable rule
&lt;/h2&gt;

&lt;p&gt;When an API exposes a batch endpoint, use it for fan-out workflows.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Promise.all()&lt;/code&gt; is useful when independent work should happen concurrently. It is dangerous when every item calls the same rate-limited external service at once.&lt;/p&gt;

&lt;p&gt;A practical checklist:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If a job fans out to many recipients, listings, files, or webhook targets, check provider rate limits before shipping it.&lt;/li&gt;
&lt;li&gt;If the provider offers batch operations, prefer them over local concurrency.&lt;/li&gt;
&lt;li&gt;If batching is not available, add an explicit queue, throttle, or retry strategy.&lt;/li&gt;
&lt;li&gt;In the UI, report provider errors in a way that distinguishes recipient failures from request-rate failures.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The general lesson is simple: local concurrency can accidentally turn a small feature into a traffic spike.&lt;/p&gt;

&lt;p&gt;The fix is not always to send slower. Sometimes it is to send in the shape the provider designed for.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>backend</category>
    </item>
    <item>
      <title>What Financial Metrics Actually Matter Right Now (And Why I Keep Second-Guessing Myself)</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Fri, 08 May 2026 04:51:34 +0000</pubDate>
      <link>https://dev.to/canceylan1988/what-financial-metrics-actually-matter-right-now-and-why-i-keep-second-guessing-myself-2523</link>
      <guid>https://dev.to/canceylan1988/what-financial-metrics-actually-matter-right-now-and-why-i-keep-second-guessing-myself-2523</guid>
      <description>&lt;h1&gt;
  
  
  What Financial Metrics Actually Matter Right Now (And Why I Keep Second-Guessing Myself)
&lt;/h1&gt;

&lt;p&gt;Weekend is near and Eevery year around this time I sit down to think through my investing strategy. And every year I notice the same thing: the framework I used last year feels slightly broken.&lt;/p&gt;

&lt;p&gt;This year is no different. But the discomfort feels sharper. We're in an environment where macro signals and market behaviour are barely speaking to each other, where AI optimism is doing a lot of heavy lifting for valuations that fundamental data alone wouldn't justify, and where I genuinely don't know whether we're witnessing a productivity revolution or building a very expensive illusion.&lt;/p&gt;

&lt;p&gt;I don't have a clean answer to that. But I've been thinking hard about which numbers are actually worth watching right now, and which ones we've been trained to worship out of habit.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(This is personal opinion and not financial advice. Do your own research before making any investment decisions.)&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Matters: The Playbook Keeps Changing
&lt;/h2&gt;

&lt;p&gt;I started investing seriously around 2018. That era had a clear religion: revenue growth above all else. Positive EBITDA was almost beside the point. If you were growing fast enough, the market believed profitability would follow. SaaS multiples were extraordinary — companies burning cash were celebrated like they'd invented fire. Marketplaces were appearing everywhere. Every week there was a new app promising to be "the Airbnb of dog grooming" or something equally ambitious.&lt;/p&gt;

&lt;p&gt;Then the pandemic distorted everything. Streaming, delivery, e-commerce, stay-at-home anything — all exploded. The numbers looked incredible, but a lot of that demand was borrowed from the future. Peloton thought it had conquered fitness. It had just borrowed everyone's lockdown boredom.&lt;/p&gt;

&lt;p&gt;2022 corrected hard. There's a well-documented historical pattern of midterm election years in the US being difficult ones for markets — and 2022 delivered on that pattern with particular conviction, with the S&amp;amp;P 500 falling roughly 19% over the year. Rate hikes, multiple compression, and a lot of portfolios humbled. My own included. Looking ahead, 2026 is the next US midterm year, and while history doesn't repeat on a schedule, it does tend to rhyme — worth keeping somewhere in the back of your mind as you think about positioning.&lt;/p&gt;

&lt;p&gt;And then, almost immediately after, the AI rocket launched.&lt;/p&gt;

&lt;p&gt;Nvidia, Microsoft, Google, Meta — infrastructure and application plays across the board. Even Intel found a reason to rally (briefly, painfully, and then less so — but still). The AI wave pulled capital into a new narrative with enormous velocity. We've been riding that wave since, and the question now is whether the fundamentals are catching up to the story, or whether the story is still running ahead of the fundamentals.&lt;/p&gt;

&lt;p&gt;For context: GDP growth expectations in Europe have been revised downward repeatedly, with the eurozone growing below 1% in recent years and forecasts remaining modest. The US has fared better, but the picture is uneven, and productivity gains from AI investment have not yet translated clearly into broader economic output. That gap is worth sitting with.&lt;/p&gt;

&lt;p&gt;Which means the usual metrics are worth questioning.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Financial Metrics Actually Matter Right Now for AI-Era Companies
&lt;/h2&gt;

&lt;p&gt;Revenue growth still matters, but it's no longer sufficient on its own. For the large, mature companies leading their sectors, user growth is largely saturated. The question has shifted from "how fast are you growing" to "how productively are you deploying capital."&lt;/p&gt;

&lt;p&gt;Here's what I'm actually watching — with real companies as reference points, though none of this is a recommendation to buy or sell anything:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ROIC (Return on Invested Capital).&lt;/strong&gt; This is becoming more interesting to me than almost anything else right now. If AI investment is genuinely making companies more efficient and more profitable, it should start showing up here over time. A company like &lt;strong&gt;Microsoft&lt;/strong&gt; is worth tracking here — it has historically maintained ROIC above 20%, and with massive Azure AI capex now flowing through, the question is whether that ratio holds or drifts. The signal is still early and noisy, but I watch it. I'm not concluding anything yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gross Margin Trend.&lt;/strong&gt; This is a cleaner signal than net margin because it strips out the noise of one-time charges, restructuring costs, and accounting choices. &lt;strong&gt;Nvidia&lt;/strong&gt; is a fascinating case — its gross margins expanded dramatically through the AI infrastructure wave, reaching above 70% in recent quarters. That's a real signal of pricing power and structural leverage. For software companies this is easier to trace. For hardware it's messier, but still worth tracking over consecutive quarters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Revenue Per Employee.&lt;/strong&gt; This is the one I keep coming back to. If AI is genuinely transforming productivity inside organisations, we should see revenue per employee climb — not just because of layoffs, but because fewer people are generating more output. &lt;strong&gt;Meta&lt;/strong&gt; is often cited here: after its "year of efficiency" in 2023, revenue per employee climbed significantly. But it's worth asking honestly how much of that was AI-driven output and how much was just aggressive headcount reduction. That's a meaningful distinction, and the earnings call transcripts usually give you enough to tell the difference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operating Leverage (for asset-heavy businesses).&lt;/strong&gt; For companies with significant fixed cost bases, the question is whether incremental revenue is dropping to the bottom line at an improving rate. Think of a company like &lt;strong&gt;Amazon Web Services&lt;/strong&gt; within Amazon's broader P&amp;amp;L — as revenue scales, do infrastructure costs grow slower? Right now I see volatile results here across the market, which makes me cautious about drawing strong conclusions. But directionally, it's worth checking whether a company's cost base is becoming more or less fixed relative to revenue growth.&lt;/p&gt;




&lt;h2&gt;
  
  
  Is the AI Rally a Bubble Like the Dot-Com Crash of 2000?
&lt;/h2&gt;

&lt;p&gt;This is the question I can't fully answer yet, and I want to be honest about that.&lt;/p&gt;

&lt;p&gt;The parallel is tempting. Enormous capital flowing into infrastructure for a transformative technology, valuations running ahead of demonstrated returns, a narrative that feels so compelling that scepticism seems almost anti-intellectual. My instinct says, that AI is so powerful and immediate that we will see a efficiency boost soon.&lt;/p&gt;

&lt;p&gt;Because there are real differences. The companies at the centre of this cycle are, by and large, already profitable. Nvidia is not Pets.com. The infrastructure being built is not hypothetical. Compute demand is real. The question is not whether AI is useful, but whether the current level of investment will generate returns commensurate with the capital deployed — and on what timeline.&lt;/p&gt;

&lt;p&gt;The PE picture is worth grounding in history. At the peak of the dot-com bubble in 2000, the S&amp;amp;P 500's trailing PE ratio reached approximately 30–33x. It then collapsed to around 15x by the mid-2000s. In the years following the 2008 financial crisis, the market bottomed around 10–13x. The long-run historical average for the S&amp;amp;P 500 sits somewhere around 15–17x trailing earnings, depending on the period you measure.&lt;/p&gt;

&lt;p&gt;Today, the S&amp;amp;P 500 trailing PE ratio is sitting in the range of 24–27x — well above the historical average, though not quite at dot-com peak territory. Forward PE ratios (based on expected earnings) are somewhat lower but still elevated. The CAPE ratio (Shiller PE), which smooths earnings over a ten-year cycle, is currently above 35x — a level that has historically preceded lower long-run returns, though it's a poor short-term timing tool.&lt;/p&gt;

&lt;p&gt;The PE compression we saw in the recent market pullback made valuations at least slightly more honest. Not cheap, but less obviously detached. Whether that compression continues — or whether earnings actually grow into current multiples — is the open question.&lt;/p&gt;

&lt;p&gt;The soft landing versus hard crash question remains genuinely open. I don't trust anyone who tells you they know the answer.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Most People Get Wrong When Reading Earnings Right Now
&lt;/h2&gt;

&lt;p&gt;They're still looking at EPS and revenue beats versus consensus, and treating that as the whole story.&lt;/p&gt;

&lt;p&gt;Consensus estimates are manufactured. Analysts lower estimates into earnings season so companies can beat them. This is not a conspiracy, it's just how the game is played. A beat against a sandbagged number tells you very little about whether the underlying business is actually healthier.&lt;/p&gt;

&lt;p&gt;The more useful exercise is to read the capital allocation section of the earnings call carefully. Where are they putting money? What is the stated expected return on that investment, and over what horizon? How much of current AI capex is discretionary versus committed? That's where the real signal lives.&lt;/p&gt;

&lt;p&gt;Also worth noting: companies are currently investing enormous sums into AI infrastructure with limited near-term return visibility. That is not inherently irrational, but it does mean the financial statements today are a lagging indicator of a bet being made on the future. Read them that way.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The market can stay irrational longer than you can stay patient, but it can also stay rational longer than your narrative can stay intact.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What to Actually Do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Watch ROIC across 3-4 consecutive quarters, not just one.&lt;/strong&gt; A single quarter of improvement means nothing. A trend means something. Set a simple tracker for the companies you hold and check it after each earnings cycle. Microsoft and Alphabet are useful benchmarks to compare against sector peers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read gross margin trends before revenue headlines.&lt;/strong&gt; Revenue can be bought with sales spend. Gross margin expansion is harder to fake and tells you more about the structural economics of the business. Nvidia's margin expansion is the reference case — hold other companies to a similar standard of scrutiny.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat revenue-per-employee improvement with scepticism until proven otherwise.&lt;/strong&gt; Ask whether it's driven by headcount reduction or genuine output growth. Meta's 2023 efficiency gains are the often-cited example — but dig into the transcript before you draw conclusions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For asset-heavy companies, calculate operating leverage manually.&lt;/strong&gt; Take revenue growth percentage and compare it to operating cost growth percentage over the same period. If costs are growing faster than revenue, the story about efficiency is not in the numbers yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't confuse infrastructure spend for immediate productivity.&lt;/strong&gt; Massive capex into AI today is a future bet, not a present result. Be patient with the timeline, but be honest about the gap between investment and return.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the 2026 midterm in the background.&lt;/strong&gt; History doesn't repeat, but midterm years have a tendency to test portfolios. Not a reason to panic — just a reason to know what you own and why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check your own narrative.&lt;/strong&gt; Every time I think I have the market figured out, it's worth asking whether I'm reading data or confirming a story I've already decided to believe. That question is uncomfortable. It's also the most useful one I know.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I'll keep watching and will write a follow-up when the picture gets clearer. Or messier. Probably messier first.&lt;/p&gt;

&lt;p&gt;If you're working through the same questions — whether it's about valuation discipline, reading earnings, or just figuring out how to think about the AI cycle — I'd genuinely enjoy the exchange. Please get in touch and let's talk it through.&lt;/p&gt;

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