<?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>AI Is the Most Equalising Force Tech Has Ever Seen, Especially for Women</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Thu, 04 Jun 2026 14:48:54 +0000</pubDate>
      <link>https://dev.to/canceylan1988/ai-is-the-most-equalising-force-tech-has-ever-seen-especially-for-women-15dk</link>
      <guid>https://dev.to/canceylan1988/ai-is-the-most-equalising-force-tech-has-ever-seen-especially-for-women-15dk</guid>
      <description>&lt;h2&gt;
  
  
  AI Is the Most Equalising Force Tech Has Ever Seen, Especially for Women
&lt;/h2&gt;

&lt;p&gt;I spent a day at an AI coding conference. Not as a speaker. Just as someone building something, paying attention, and occasionally wondering whether I was the least technical person in the room. The conversations were sharp. Some were uncomfortable. But one thread kept pulling at me, and it was not the one everyone expected.&lt;/p&gt;

&lt;p&gt;Nobody had clean answers. And that was actually useful.&lt;/p&gt;

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

&lt;p&gt;Let me be honest about what most AI-in-tech conversations look like from the outside. Confident takes. Either the robots are coming for everything, or nothing is really changing and you should relax. The conference was not like that. It was full of people actively building things, actively unsure, and actively debating. That felt closer to reality.&lt;/p&gt;

&lt;p&gt;What I took away is not a prediction. It is more of a reframe. The question is not whether AI will change the job market. It obviously will. The more interesting question is who it changes things &lt;em&gt;for&lt;/em&gt;, and in which direction.&lt;/p&gt;

&lt;p&gt;The answer that kept coming back to me: the people who have historically been locked out. And at the top of that list, women.&lt;/p&gt;

&lt;h2&gt;
  
  
  Will AI replace software developers?
&lt;/h2&gt;

&lt;p&gt;Short answer: not the ones paying attention.&lt;/p&gt;

&lt;p&gt;The longer version is genuinely complicated. The role of Junior developers got some airtime at this conference, and not in a reassuring way. Some argue that entry-level roles will shrink because AI can now handle the scaffolding work that used to train those people. Others argue the opposite: that juniors who embrace AI tooling are actually more valuable right now, because they arrive without years of habits to unlearn and with a genuine appetite for the new workflow.&lt;/p&gt;

&lt;p&gt;Both arguments are probably true in different contexts. What seems less debatable is that the shape of the job is changing faster than job descriptions are. About 80% of enterprise AI projects reportedly fail, and the most cited reason is not the technology. It is people inside organisations who are not willing to leave their comfort zone, including, maybe especially, in leadership positions. The bottleneck is human, not algorithmic.&lt;/p&gt;

&lt;p&gt;So the developer who learns to work &lt;em&gt;with&lt;/em&gt; AI is not being replaced. They are being promoted past everyone who refused to adjust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can you build real things with AI if you are not a developer?
&lt;/h2&gt;

&lt;p&gt;This is the one that kept me up a little.&lt;/p&gt;

&lt;p&gt;Vibe coding, the practice of building working software through natural language prompts without writing traditional code, is not a gimmick anymore. It is clumsy in places, and I would not hand it a critical infrastructure project tomorrow. But the distance between an idea and a working prototype has collapsed. Significantly.&lt;/p&gt;

&lt;p&gt;What surprised me at the conference was who was least threatened by this. The coding bootcamps, the certification platforms, the learn-to-code institutions. You might expect them to be nervous. Most were not. Their argument: understanding the fundamentals still matters, because when the AI gets it wrong, you need to know what wrong looks like. The tool accelerates. It does not replace the need for judgment.&lt;/p&gt;

&lt;p&gt;I am not fully convinced, but I am not dismissing it either. I'll keep watching that space and report back.&lt;/p&gt;

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

&lt;p&gt;Everyone is framing this as a loss. Job cuts. Skill obsolescence. Industry disruption. That framing is real, but it is not the whole picture, and it is suspiciously centred on people who already had the advantage.&lt;/p&gt;

&lt;p&gt;The conversation that landed hardest for me was about women in tech. The argument, and I think it is a strong one, is that AI is one of the most significant equalising forces the industry has ever seen. Women have historically been underrepresented in tech roles, partly because of gatekeeping, partly because of credentials culture, partly because of how networking and sponsorship work in deeply homogenous industries. AI disrupts all three simultaneously.&lt;/p&gt;

&lt;p&gt;When the barrier to building something is no longer five years of computer science, but curiosity and the ability to ask the right questions, the landscape changes. The same logic applies to migrants, to people without elite degrees, to people in regions that were never close to a Silicon Valley pipeline.&lt;/p&gt;

&lt;p&gt;Globally, women hold around 26% of tech jobs, a number that has barely moved in a decade. The gender pay gap in tech sits somewhere between 16 and 28% depending on the market. Those are stubborn numbers. But when the tools change, the gatekeepers lose some of their leverage.&lt;/p&gt;

&lt;p&gt;This is worth sitting with. The technical debt that kept most people on the outside of the digital economy, the years of learning, the access to the right networks, the cost of entry, is being vaporised. Not completely. Not overnight. But faster than any previous shift in computing.&lt;/p&gt;

&lt;p&gt;And here is the asymmetry that nobody talks about enough: individuals can adapt faster than organisations. A person can learn a new tool this weekend. A corporation with legacy infrastructure, internal politics, and a middle management layer that is quietly terrified has to navigate all of that before it can even run a proper pilot. That is a structural advantage that currently sits with small teams, independent builders, and people who have nothing to unlearn.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every structural advantage that kept people out of tech is slowly becoming a software problem, and AI just shipped the patch.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A note on Fleamio
&lt;/h2&gt;

&lt;p&gt;One thing I can speak to directly is what this shift looks like in practice for a small team. At Fleamio, we are a lean operation. We are not a legacy enterprise. We do not have layers of internal politics slowing down every experiment. When a new AI tool lands, we can pick it up, test it against a real problem, and decide within days whether it belongs in the workflow. That agility is not incidental. It is one of the few genuine structural advantages a small team holds over a funded competitor right now. The window where moving fast actually matters is open. We are using it.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;If you are early in your career:&lt;/strong&gt; do not wait for clarity. The people debating whether to learn AI tooling are already behind the people using it. Pick one tool, build one real thing with it, understand where it breaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you have been out of tech or never felt like it was for you:&lt;/strong&gt; this is the most genuinely open window in a generation. The qualification inflation that kept the door closed for years is losing its grip. The entry cost has never been lower.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you are inside a large organisation:&lt;/strong&gt; the 80% failure rate on AI projects is almost certainly downstream of culture, not capability. The question to push internally is not "what tools are we buying" but "what are we actually willing to change about how decisions get made."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you are building something independently:&lt;/strong&gt; your advantage over incumbents is real right now, but it is time-limited. Organisations are slow, but they are not asleep. The window where a small team can move 10x faster than a funded competitor is open. Act accordingly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On the pay gap question:&lt;/strong&gt; if you are a woman, a migrant, or anyone who has been told the cost of entry into tech was too high, it is worth genuinely reassessing that right now. The tools changed. The old map is not reliable anymore.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>techai</category>
    </item>
    <item>
      <title>Do not build masks from translucent polygons</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Sat, 30 May 2026 04:51:37 +0000</pubDate>
      <link>https://dev.to/canceylan1988/do-not-build-masks-from-translucent-polygons-1ee0</link>
      <guid>https://dev.to/canceylan1988/do-not-build-masks-from-translucent-polygons-1ee0</guid>
      <description>&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;A map needed a fog-of-war effect: areas with data should feel visible, and areas without data should stay dark.&lt;/p&gt;

&lt;p&gt;The first implementation used circular buffer polygons around every data point. Each buffer had soft transition rings with different opacities. In isolation, one buffer looked reasonable.&lt;/p&gt;

&lt;p&gt;But once multiple data points were close together, the map turned into a mess of dark circular bands. The visual effect looked like stacked shadows instead of explored territory.&lt;/p&gt;

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

&lt;p&gt;The mistake was using many translucent polygons for a masking problem.&lt;/p&gt;

&lt;p&gt;Map renderers blend every semi-transparent shape independently. If three rings overlap, their opacity stacks. If ten rings overlap, the area becomes much darker than intended.&lt;/p&gt;

&lt;p&gt;The system was technically doing the right thing. The model was wrong.&lt;/p&gt;

&lt;p&gt;A fog effect should answer one question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How visible should this pixel be?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The polygon approach answered a different question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How many semi-transparent shapes happen to cover this pixel?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those are not equivalent.&lt;/p&gt;

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

&lt;p&gt;The bug hides because the first few test cases look fine.&lt;/p&gt;

&lt;p&gt;One circle creates a nice transition. Two circles still seem acceptable. Then real data arrives, nearby buffers overlap, and the renderer starts accumulating darkness in unpredictable-looking bands.&lt;/p&gt;

&lt;p&gt;This is especially easy to miss in AI-assisted UI work because the proposed solution sounds geospatially correct: "create buffer zones around covered areas." But the desired output was not a geographic fact. It was a visual effect.&lt;/p&gt;

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

&lt;p&gt;Use a single composited mask.&lt;/p&gt;

&lt;p&gt;The corrected implementation used a canvas layer above the map:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Draw one dark rectangle over the whole viewport.&lt;/li&gt;
&lt;li&gt;For each covered area, subtract a soft radial gradient from that dark layer.&lt;/li&gt;
&lt;li&gt;Redraw the canvas when the map moves, zooms, resizes, or receives new data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now overlaps reveal more of the map instead of adding more darkness.&lt;/p&gt;

&lt;p&gt;The important change was not canvas itself. It was changing the mental model:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One mask, many cutouts.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;blockquote&gt;
&lt;p&gt;Many transparent overlays.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;Use map polygons for geographic facts.&lt;/p&gt;

&lt;p&gt;Use canvas, WebGL, or another compositing layer for visual masks.&lt;/p&gt;

&lt;p&gt;Good uses for polygons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;boundaries&lt;/li&gt;
&lt;li&gt;regions&lt;/li&gt;
&lt;li&gt;hex cells&lt;/li&gt;
&lt;li&gt;selected areas&lt;/li&gt;
&lt;li&gt;measured buffers that represent real spatial meaning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bad use for overlapping translucent polygons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fog-of-war&lt;/li&gt;
&lt;li&gt;spotlight effects&lt;/li&gt;
&lt;li&gt;progressive reveal&lt;/li&gt;
&lt;li&gt;darkness/lightness masks&lt;/li&gt;
&lt;li&gt;any effect where opacity should not depend on shape count&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the overlay is supposed to behave like a mask, build a mask. Do not approximate it with many semi-transparent geometries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical test
&lt;/h2&gt;

&lt;p&gt;Before shipping a visual map overlay, test it with clustered data.&lt;/p&gt;

&lt;p&gt;Sparse points can hide blending bugs. Dense points reveal whether the effect is truly composited or just stacked.&lt;/p&gt;

&lt;p&gt;If adding more data makes the overlay darker, louder, or ring-shaped, the rendering model is wrong.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Claude Is Overhyped. Codex Is Underrated. Here Is What Actually Happened When I Built Real Products With Both.</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Fri, 29 May 2026 20:55:46 +0000</pubDate>
      <link>https://dev.to/canceylan1988/claude-is-overhyped-codex-is-underrated-here-is-what-actually-happened-when-i-built-real-products-33bb</link>
      <guid>https://dev.to/canceylan1988/claude-is-overhyped-codex-is-underrated-here-is-what-actually-happened-when-i-built-real-products-33bb</guid>
      <description>&lt;h2&gt;
  
  
  The picture looks different now
&lt;/h2&gt;

&lt;p&gt;A few months ago I &lt;a href="https://dev.toLINK_TO_ORIGINAL"&gt;wrote about hiring two AI developers&lt;/a&gt; and the surprisingly human feeling of having to manage them. At the time I was still figuring out the dynamic. Now I have actually shipped products with both of them running in parallel, and the picture looks different. Not worse. Just sharper.&lt;/p&gt;

&lt;p&gt;This is not a Claude takedown. I want to be clear about that from the start. Claude is the reason I broke through a years-long wall of having ideas with no real way to implement them. That matters to me personally. But after running both agents through real production cycles, I have some honest things to say about where each one earns its money and where the hype does not hold up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters if you are building as a solo founder or small team
&lt;/h2&gt;

&lt;p&gt;Most AI coding conversations are still about single-agent setups. Someone opens Cursor or Claude, asks it to build something, and judges the tool by what comes out. That is a fine place to start. But once you are building products with actual users, real infrastructure, and moving parts that interact with each other, the single-agent model starts to show its limits.&lt;/p&gt;

&lt;p&gt;The moment I started treating these two tools as roles rather than alternatives, everything got faster and more stable. The question is not "which AI is better." The question is: better at what, and at which stage?&lt;/p&gt;

&lt;h2&gt;
  
  
  How does Claude perform for building real products?
&lt;/h2&gt;

&lt;p&gt;Still exceptional for the right tasks. Claude is my frontend developer and UX partner, full stop. Its design instincts are genuinely younger and fresher than anything Codex produces. When I give it a layout brief or a component to build, what comes back tends to feel modern, clean, and considered. It does not need much direction on visual hierarchy.&lt;/p&gt;

&lt;p&gt;For heavy implementation work, especially in the middle phases of building an MVP, Claude handles multitasking well. You can hand it several things at once and it manages them with decent coherence. Speed is real. If I need a working prototype that looks like a product rather than a proof of concept, Claude gets me there faster than anything else I have used.&lt;/p&gt;

&lt;p&gt;But here is the honest limitation. Claude is not great at knowing what it does not know. When something breaks at the deployment layer or the server behaves unexpectedly, Claude reasons from the code rather than from observed behaviour. It guesses intelligently, but it is still guessing. For debugging live environments, that gap becomes expensive quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does Codex actually do better than Claude?
&lt;/h2&gt;

&lt;p&gt;More than people give it credit for, particularly at the stages most developers skip or rush.&lt;/p&gt;

&lt;p&gt;I now start every project with Codex, not Claude. The planning and architecture phase is where it genuinely surprises me. It thinks like a product manager and a solutions architect at the same time, identifies edge cases before they exist, maps dependencies, and produces infrastructure decisions that hold up later. The plans need very little correction mid-build. That saves an enormous amount of back-and-forth downstream.&lt;/p&gt;

&lt;p&gt;The second place Codex outperforms is testing and deployment. This is the capability that I think is most underrated in public discussion. Codex can actually observe what is happening on a live server. It takes screenshots. It checks whether the implementation works in production, not just whether the code looks correct. That is a fundamentally different kind of intelligence for debugging. Claude tells you what the code says. Codex tells you what the product is actually doing.&lt;/p&gt;

&lt;p&gt;Codex also thinks more carefully about legal and compliance dimensions of code. Not perfectly, but noticeably. For anyone building in regulated contexts or even just thinking about data handling, it flags things that Claude would sail past without comment.&lt;/p&gt;

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

&lt;p&gt;They treat it as a competition. They pick a side, defend it, and miss the point entirely.&lt;/p&gt;

&lt;p&gt;The workflow that is actually working for me looks like this: Codex plans and architects, Claude builds the frontend and implements the core product, both review each other's work (yes, I run cross-reviews deliberately, the 360-degree perspective catches things neither would catch alone), and then Codex takes the lead again for testing, deployment, and server-side debugging.&lt;/p&gt;

&lt;p&gt;The uncomfortable truth is that neither agent is dominant across the full stack. Claude is not the rocket I called it in the original article, it is more like a brilliant junior frontend architect. Codex is not just the wiring-checker, it is closer to a senior engineer who works more slowly but breaks fewer things in production.&lt;/p&gt;

&lt;p&gt;The hype around Claude specifically tends to come from people who are building prototypes and demos. Which is valid. Claude is exceptional at that. But once you are building something real, the production reliability of Codex starts to matter in ways that demo culture does not reward.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The best AI workflow is not about picking a winner. It is about knowing which problem each agent was actually built to solve.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I am still testing things here, including some newer capabilities I have heard about but not fully explored yet, particularly around how Codex handles longer-context architectural reasoning across large codebases. That is next on my list and I will keep people posted as the stack evolves.&lt;/p&gt;

&lt;p&gt;I also write about the product thinking side of building with AI tools. If you are interested in how the solo builder mindset connects to product strategy, that territory overlaps with what I cover in my notes on AI and product development.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start every build with Codex, not Claude.&lt;/strong&gt; Use it to define your architecture, plan your data model, and map your infrastructure before a single line of code is written. The time you spend here is recovered threefold later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hand the frontend and MVP implementation to Claude.&lt;/strong&gt; Give it the vision that Codex defined and let it build fast. Do not micromanage the design decisions, it has better instincts there than you expect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run cross-reviews deliberately.&lt;/strong&gt; Let Claude review Codex's plans. Let Codex review Claude's implementation. The friction between the two agents surfaces things you would not find alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Return to Codex at deployment.&lt;/strong&gt; Do not try to debug live server behaviour with Claude. It is not built for that. Codex can observe the running product, not just the codebase, and that distinction matters when something is wrong in production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resist the urge to pick one tool.&lt;/strong&gt; The efficiency gain from a dual setup is not marginal. It is structural. The two agents cover genuinely different parts of the software development lifecycle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat the workflow as a living thing.&lt;/strong&gt; Both tools are updating regularly. What is true about their relative strengths at the time of writing this may shift. Build the habit of re-evaluating, not just defending the setup that worked last month.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The solo builder era is real. But it works best when you stop thinking about AI tools as replacements for a dev team and start thinking about them as a dev team with distinct roles that you actually have to manage.&lt;/p&gt;

</description>
      <category>techai</category>
    </item>
    <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>
  </channel>
</rss>
