<?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: Doogal Simpson</title>
    <description>The latest articles on DEV Community by Doogal Simpson (@doogal).</description>
    <link>https://dev.to/doogal</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3657111%2Fac69c96a-33e1-4023-99ef-ee3059b3ccb6.jpeg</url>
      <title>DEV Community: Doogal Simpson</title>
      <link>https://dev.to/doogal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/doogal"/>
    <language>en</language>
    <item>
      <title>The To-Do Method: Control AI Coding Agents in Your IDE</title>
      <dc:creator>Doogal Simpson</dc:creator>
      <pubDate>Sat, 25 Jul 2026 10:52:26 +0000</pubDate>
      <link>https://dev.to/doogal/the-to-do-method-control-ai-coding-agents-in-your-ide-3fpj</link>
      <guid>https://dev.to/doogal/the-to-do-method-control-ai-coding-agents-in-your-ide-3fpj</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR: Tired of AI agents writing sloppy, off-target code? The "To-Do" method restores fine-grained control inside your IDE. By dropping explicit inline comments (like &lt;code&gt;// TODO: agent - refactor this&lt;/code&gt;) directly into your files, you can guide agents through iterative, high-standard implementations while you review and queue up tasks in real time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you delegate a feature to an AI coding agent, you often end up spending more time fixing its architectural shortcuts than you would have spent writing the code from scratch. The agent produces code that technically runs, but fails your standard for naming, structure, or performance. Instead of fighting with complex prompts in a chat window, you can manage agent code quality directly in your files using the &lt;strong&gt;To-Do Method&lt;/strong&gt;. It treats the agent like a hyper-fast pair programmer who just needs constant, highly localized steering inside your editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the To-Do Method for AI Agents?
&lt;/h2&gt;

&lt;p&gt;The To-Do Method is a workflow where you direct an AI agent entirely through structured comments left directly inside your codebase. Instead of writing external prompts, you write inline notes, tell the agent to run, and then review and iterate on its output in place.&lt;/p&gt;

&lt;p&gt;Think of it like leaving red-pen edits on a draft. You drop highly targeted instructions right where the work needs to happen. For example, you can write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// TODO: agent - Write a function that returns true if email contains an @ symbol.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isValidEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You point the agent to your file and say, "Go fix the comments." The agent scans the code, finds your markers, replaces them with code, and stops.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do You Keep AI Code Up to Your Engineering Standards?
&lt;/h2&gt;

&lt;p&gt;You maintain high standards by using tight, iterative feedback loops and treating agent-generated code as a draft that you ruthlessly critique. When an agent delivers code that isn't up to par, you do not rewrite it yourself; you write a comment telling the agent exactly why it failed and how to refactor it.&lt;/p&gt;

&lt;p&gt;For example, if an agent writes a sprawling, unreadable nested conditional, don't accept the PR. Drop a comment directly above it:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;// TODO: agent - This implementation is too complex. Refactor this using guard clauses and extract the validation logic into a helper function.&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The To-Do Method Lifecycle
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Developer Action&lt;/th&gt;
&lt;th&gt;Agent Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1. Direct&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Write explicit &lt;code&gt;TODO: agent&lt;/code&gt; comments in the codebase.&lt;/td&gt;
&lt;td&gt;Idle.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2. Execute&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Trigger the agent and point it to the modified files.&lt;/td&gt;
&lt;td&gt;Reads comments, replaces them with code, and removes the TODOs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3. Critique&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Review the diff. If it's messy, write a new &lt;code&gt;TODO: agent&lt;/code&gt; to refactor.&lt;/td&gt;
&lt;td&gt;Idle.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;4. Parallelize&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Write TODOs in File B while the agent works on File A.&lt;/td&gt;
&lt;td&gt;Implements File A.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This method pairs beautifully with Test-Driven Development (TDD). You can write your test cases first, leave a comment like &lt;code&gt;// TODO: agent - Make these tests pass using the strategy pattern&lt;/code&gt;, and let the agent do the manual labor while you watch the test runner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can You Work in Parallel with an AI Agent?
&lt;/h2&gt;

&lt;p&gt;Yes, you can work in parallel with an AI agent by queueing up tasks in different files while the agent is executing your previous instructions. Because the agent is confined to the specific files and comments you tell it to watch, you can stay focused in another part of the editor.&lt;/p&gt;

&lt;p&gt;Imagine you are building a complex checkout system. While the agent is busy implementing a calculated tax helper in your service layer, you can move ahead to the controller or the frontend components, writing your next set of &lt;code&gt;TODO: agent&lt;/code&gt; comments. By the time you finish mapping out the architecture, the agent has finished the boilerplate, and you are ready to review its work. It keeps your hands on the keyboard and your mind focused on system design.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I prevent the AI agent from deleting my manual comments?
&lt;/h3&gt;

&lt;p&gt;Clearly prefix your instructions (e.g., &lt;code&gt;// TODO: agent - [instruction]&lt;/code&gt;) and specify in your system instructions or agent settings that it should only remove a comment once the requested implementation is fully complete and verified.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does this method work with all AI coding assistants?
&lt;/h3&gt;

&lt;p&gt;Yes, this workflow works with any agentic IDE tool (like Cursor, Copilot Workspace, or Windsurf) that can read your workspace context and modify files based on natural language commands.&lt;/p&gt;

&lt;h3&gt;
  
  
  Isn't writing inline comments slower than just chatting with the AI?
&lt;/h3&gt;

&lt;p&gt;While it takes a few extra seconds to type comments in-file, you save time by keeping the agent focused on precise context, eliminating the need to copy-paste code back and forth between your IDE and a browser.&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>aicoding</category>
      <category>developertools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Game Theory in Software Engineering: How Cooperation Wins</title>
      <dc:creator>Doogal Simpson</dc:creator>
      <pubDate>Thu, 23 Jul 2026 10:56:16 +0000</pubDate>
      <link>https://dev.to/doogal/game-theory-in-software-engineering-how-cooperation-wins-26gk</link>
      <guid>https://dev.to/doogal/game-theory-in-software-engineering-how-cooperation-wins-26gk</guid>
      <description>&lt;p&gt;&lt;strong&gt;In software engineering, optimizing for individual metrics at the expense of your team is a losing strategy. I believe that while "selfish" coding yields quick short-term wins, long-term career compounding and technical success come exclusively from consistent, reputation-building cooperation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I've always found game theory fascinating, especially when it maps directly to how we build software. Imagine walking into a room where you can grab a quick fifty quid by taking advantage of someone, or earn a single pound by cooperating. If you are only playing this game once, the selfish move makes mathematical sense. &lt;/p&gt;

&lt;p&gt;But in software engineering, I see devs make the mistake of treating their daily work like a single-play game when it is actually a long, repeated interaction. The codebase is the room, and your engineering team is the crowd watching your every commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does game theory matter in software development teams?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;I've observed that game theory, specifically the Iterated Prisoner's Dilemma, directly models how trust and collaboration scale in technical teams. While a developer can temporarily boost their personal metrics by ignoring team needs, I find that this selfish behavior ultimately destroys the trust required for long-term career growth. Consistent cooperation creates a compounding network effect where everyone wins.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let’s look at how this plays out in a normal sprint. If you decide to work in a complete silo—cranking out features, ignoring junior developers, and skipping thorough code reviews to inflate your personal ticket count—you are playing a single-round game. You might get the temporary satisfaction of looking highly productive to a manager who only looks at Jira charts.&lt;/p&gt;

&lt;p&gt;But I often see this backfire when you need help debugging a complex production issue, or when you need a crucial pull request reviewed quickly. Your team remembers your past choices, and they naturally adapt. Cooperating with a non-cooperative player is a losing strategy, so your teammates will eventually stop prioritizing your requests, leaving your work stalled.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the difference between single-round and iterated games in engineering?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;From what I've seen, a single-round game assumes you will never interact with the other party again, making selfish optimization highly rational. In contrast, an iterated game relies on repeated interactions where players remember past choices, making cooperation the only strategy that yields compounding, long-term rewards.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I like to break down specific engineering choices to show how they play out across these two strategies:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engineering Action&lt;/th&gt;
&lt;th&gt;Single-Round Strategy (Selfish)&lt;/th&gt;
&lt;th&gt;Iterated Game Strategy (Cooperative)&lt;/th&gt;
&lt;th&gt;Long-Term Career Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pull Request Reviews&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rush through with a generic "LGTM" to get back to your own tasks.&lt;/td&gt;
&lt;td&gt;Leave thoughtful, constructive feedback and help mentor the author.&lt;/td&gt;
&lt;td&gt;High trust, cleaner codebase, and faster reviews when you submit your own PRs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Knowledge Sharing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hoard domain knowledge to make yourself seem indispensable.&lt;/td&gt;
&lt;td&gt;Document systems, write clean runbooks, and actively pair-program.&lt;/td&gt;
&lt;td&gt;Reduced burnout, seamless delegation, and clear readiness for leadership roles.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Technical Debt&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ship hacky code quickly to hit individual sprint deadlines.&lt;/td&gt;
&lt;td&gt;Write clean, maintainable code and refactor nearby debt where possible.&lt;/td&gt;
&lt;td&gt;Shorter lead times, fewer production bugs, and a team that actually enjoys working with you.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How does team-level cooperation drive individual developer success?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;I believe that while cooperating reduces your personal daily output slightly, it exponentially raises the productivity of the entire team. Because engineering is a team sport, I am convinced that being part of a highly successful team is far more career-advancing than being a high-performer on a failing team.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I think of this as a compounding interest rate. Earning that "one pound" of mutual trust every day doesn't look like much on a random Tuesday. But over a year, a team operating on high trust moves incredibly smoothly. &lt;/p&gt;

&lt;p&gt;If you choose to help a teammate unblock their local environment instead of writing your next function, you might close one fewer ticket this week. But you have built a critical node in a resilient network. In my experience, when the team succeeds, everyone's stock rises. Hiring managers and tech leads do not look for the lone wolf who wrote thousands of lines of unmaintainable code; they look for the force multiplier who made the entire department better.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do you handle a teammate who is consistently playing a selfish game?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;I recommend using the classic game theory strategy of "tit-for-tat with forgiveness." You should mirror their boundary by not over-extending to bail them out of self-inflicted blocks, but immediately resume cooperation the moment they show collaborative behavior.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What are the risks of over-cooperating in a low-trust engineering culture?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;I've noticed that over-cooperating in an environment where leadership only rewards individual output can lead to severe burnout and low performance ratings. If your organization's performance rubrics do not value collaboration, I suggest balancing team support with visible individual contributions while searching for a healthier team culture.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Can you automate cooperation in a software delivery pipeline?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;I believe you can bake cooperative behavior into your team's workflow through automated guardrails like linting, automated testing, and branch protection. By codifying standard practices, you remove the friction of personal negotiations and make cooperative coding the default path of least resistance.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>gametheory</category>
      <category>technicalleadership</category>
      <category>careergrowth</category>
    </item>
    <item>
      <title>8 Levels of AI Autonomy in Software Engineering</title>
      <dc:creator>Doogal Simpson</dc:creator>
      <pubDate>Thu, 23 Jul 2026 10:25:20 +0000</pubDate>
      <link>https://dev.to/doogal/8-levels-of-ai-autonomy-in-software-engineering-2880</link>
      <guid>https://dev.to/doogal/8-levels-of-ai-autonomy-in-software-engineering-2880</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR: AI tooling in software engineering is a spectrum, not a binary switch. We are moving from simple inline autocompletion to managing parallel agents, building hierarchical \"Gas Town\" setups, and eventually operating \"dark factories\" where autonomous systems write, deploy, and self-heal codebases based directly on production metrics.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Working in software right now is weird. The industry is obsessed with AI, but most developers are stuck thinking about it as a binary choice: either you use Copilot to write boilerplate slightly faster, or you get replaced by a bot. &lt;/p&gt;

&lt;p&gt;It is not that simple. We are looking at a clear spectrum of autonomy—and understanding where you sit on it is the only way to avoid drowning in the noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do AI tools differ in software development?
&lt;/h2&gt;

&lt;p&gt;AI tools differ primarily by their degree of autonomy and the scope of context they can access. At the low end, they operate as passive assistants on a single line of code; at the high end, they act as autonomous systems capable of diagnosing issues, writing code, and deploying software without human intervention.&lt;/p&gt;

&lt;p&gt;To map out this landscape, we can categorize AI integration into eight distinct levels of maturity:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;What you actually do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Spicy Autocomplete&lt;/td&gt;
&lt;td&gt;Tab-completion&lt;/td&gt;
&lt;td&gt;Type code, hit tab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Agent Advisor&lt;/td&gt;
&lt;td&gt;Smart search &amp;amp; codebase querying&lt;/td&gt;
&lt;td&gt;Ask questions, read output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Pair Programmer&lt;/td&gt;
&lt;td&gt;Collaborative task execution&lt;/td&gt;
&lt;td&gt;Code and review line-by-line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Agent Manager&lt;/td&gt;
&lt;td&gt;Spins up parallel worker bots&lt;/td&gt;
&lt;td&gt;Feed tasks, triage PRs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Manager of Managers&lt;/td&gt;
&lt;td&gt;Hierarchical agent networks&lt;/td&gt;
&lt;td&gt;Rule from the top (Gas Town)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Light Factory&lt;/td&gt;
&lt;td&gt;Ticket-to-PR automated pipeline&lt;/td&gt;
&lt;td&gt;Manually sign off on changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Dark Factory&lt;/td&gt;
&lt;td&gt;Fully automated deployments&lt;/td&gt;
&lt;td&gt;Watch the pipeline run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Closed-Loop System&lt;/td&gt;
&lt;td&gt;Self-healing production loops&lt;/td&gt;
&lt;td&gt;Design the system's guardrails&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What are the different levels of AI integration for developers?
&lt;/h2&gt;

&lt;p&gt;Integration levels scale by delegating decision-making power away from the human keyboard. It begins with micro-tasks like writing helper functions and climbs to structural orchestration where you manage multiple agent workloads at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is there a difference between AI pair programming and AI agent management?
&lt;/h3&gt;

&lt;p&gt;Yes, AI pair programming involves a single developer and a single AI agent collaborating on a single task in real-time. Agent management, however, elevates the developer to a coordinator role, feeding distinct tasks to multiple agents working simultaneously across different parts of a codebase.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Level 1: Spicy Autocomplete.&lt;/strong&gt; This is the baseline. You write code, hit tab, and get a single-line or single-block suggestion. It is low-risk and has virtually zero global context.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Level 2: Agent Advisor.&lt;/strong&gt; The AI isn’t writing code here. Instead, it is a search engine on steroids. You use it to parse legacy codebases or figure out how to wire up a tricky integration, saving you thirty minutes of digging through outdated documentation.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Level 3: Pair Programmer.&lt;/strong&gt; You and a single agent tag-team a ticket. You write the interface, it stubs out the implementation, you fix its hallucinations, and it generates the tests. You are both actively working on the same branch at the same time.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Level 4: Agent Manager.&lt;/strong&gt; You stop coding in the traditional sense. You coordinate multiple agents simultaneously. One refactors a utility library, another writes API docs, and a third patches a dependency. You aren't writing code; you are feeding them work and triaging their output.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Level 5: Manager of Managers (The \"Gas Town\" Pyramid).&lt;/strong&gt; This is the &lt;em&gt;Gas Town&lt;/em&gt; setup. Instead of micromanaging five individual worker bots, you sit at the top of the pyramid and manage a single \"boss\" agent. This manager agent spins up, coordinates, and reviews its own sub-agents to execute broad project goals. You just hand down the decree from above.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is a \"dark factory\" in software engineering?
&lt;/h2&gt;

&lt;p&gt;A \"dark factory\" is an entirely automated production line where issues or features go in, and deployable code comes out with zero human intervention. The final iteration is a closed-loop system that monitors its own errors and self-heals in production.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Level 6: Light Factory.&lt;/strong&gt; A ticket lands in your project tracker, agents pick it up, run the builds, write the code, and open a PR. A human still sits at the gate, reading the diff and hitting the \"merge\" button.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Level 7: Dark Factory.&lt;/strong&gt; The human gatekeeper is removed. The pipeline is robust enough to autonomously test, build, and deploy the agent's work directly to production. If it passes the automated tests, it goes live.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Level 8: Closed-Loop Systems.&lt;/strong&gt; The final stage closes the feedback loop. Agents watch APM metrics and log pipelines. If an endpoint starts throwing 500s or latency spikes, the observing agent automatically generates a ticket, assigns it to a worker agent, builds a hotfix, tests it, and deploys it. It is self-repairing software.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Knowing where you sit on this spectrum helps you prepare for what's next. Whether you are currently leveraging spicy autocomplete or building hierarchical agent networks, the goal remains the same: knowing when to write the code yourself and when to hand over the keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I transition my team from Level 3 (Pair Programming) to Level 4 (Agent Management)?
&lt;/h3&gt;

&lt;p&gt;You have to stop writing code and accept that your job is now writing specs and QA tests. If you cannot write a perfectly clear, unambiguous API specification and pair it with rigorous integration tests, Level 4 will just generate massive amounts of parallel technical debt that you will have to manually clean up later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are \"dark factories\" actually realistic, or is it just vaporware?
&lt;/h3&gt;

&lt;p&gt;They are realistic for trivial, highly-templated tasks, but incredibly risky for complex business domains. To make them work without breaking your product, you have to invest heavily in bulletproof integration suites, sandbox environments, and instant rollback triggers—otherwise, the bot will deploy broken code at machine speed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which codebases are the easiest to automate with agents?
&lt;/h3&gt;

&lt;p&gt;Rigid, heavily typed codebases with loud compiler errors. Statically typed languages like Go or Rust give agents immediate feedback loops when they break things. If you try to run an advanced agent pipeline on a massive, dynamic, weakly typed legacy JavaScript codebase, it will hallucinate itself into a corner in minutes.&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>ai</category>
      <category>automation</category>
      <category>devops</category>
    </item>
    <item>
      <title>ETL vs ELT: Why Cheap Cloud Storage Changed Everything</title>
      <dc:creator>Doogal Simpson</dc:creator>
      <pubDate>Wed, 22 Jul 2026 10:52:06 +0000</pubDate>
      <link>https://dev.to/doogal/etl-vs-elt-why-cheap-cloud-storage-changed-everything-459c</link>
      <guid>https://dev.to/doogal/etl-vs-elt-why-cheap-cloud-storage-changed-everything-459c</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR: Traditional ETL (Extract, Transform, Load) processed data before saving it to minimize expensive storage. Today, dirt-cheap cloud storage has flipped this paradigm to ELT (Extract, Load, Transform). By loading raw data first and transforming it later, engineers prevent data loss, simplify pipeline debugging, and easily adapt to changing business requirements.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine brewing a giant pot of coffee for a crowded room, but you are forced to mix in the exact ratio of milk and sugar for every single person before you pour it into their mugs. If you miscalculate someone's preference, or if the milk curdles mid-pour, you have to dump the entire batch down the sink and start over. &lt;/p&gt;

&lt;p&gt;That is traditional ETL. You extract raw data, force it through a rigid transformation middleman, and only then load it into your database. &lt;/p&gt;

&lt;p&gt;Today, we do the opposite. We pour everyone a cup of raw, black coffee first, and let them customize it at the table. This is ELT (Extract, Load, Transform). This architectural shift isn't just a trend; it is a direct consequence of how changing cloud economics reshape system design.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the difference between ETL and ELT?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;ETL (Extract, Transform, Load) cleanses and structures data on an external staging server before saving it to a destination database. ELT (Extract, Load, Transform) dumps raw, unmodified data directly into a cloud data warehouse first, postponing all transformations until they are actually needed. This shift preserves the original data state and offloads processing to modern, highly scalable database engines.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Historically, our system architectures were constrained by the high cost of spinning disks. Storage was expensive, so databases had to be lean. We couldn't afford to store messy, redundant, or uncompressed raw JSON payloads. The transformation step was a gatekeeper, stripping away "unnecessary" data to keep the database footprint as small as possible.&lt;/p&gt;

&lt;p&gt;Today, cloud data warehouses like Snowflake, BigQuery, and Redshift have decoupled compute from storage, making raw storage virtually free. Because we no longer need to be conservative with space, we can dump raw, unstructured datasets straight into our environments and transform them on demand using SQL or tools like dbt.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;ETL (Extract, Transform, Load)&lt;/th&gt;
&lt;th&gt;ELT (Extract, Load, Transform)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Primary Driver&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High storage costs; limited DB compute&lt;/td&gt;
&lt;td&gt;Cheap cloud storage; massive DB compute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transformation Location&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Separate middleman/staging server&lt;/td&gt;
&lt;td&gt;Target data warehouse/lakehouse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Loss Risk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High (raw data is discarded pre-load)&lt;/td&gt;
&lt;td&gt;Minimal (raw data is permanently preserved)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Flexibility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rigid (schema changes require pipeline rewrites)&lt;/td&gt;
&lt;td&gt;Agile (queries are rewritten, data stays intact)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why did cheap cloud storage kill ETL?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Historically, high database storage costs forced engineers to compress and transform data before saving it to avoid ballooning bills. As cloud object storage made storage cheap, the economic bottleneck shifted from hardware constraints to developer time and data flexibility. It became significantly cheaper to store everything first and define schemas later.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When storage was a premium resource, we spent countless engineering hours designing complex staging pipelines. If business requirements changed and you needed a field you discarded six months ago, you were out of luck. The data was gone.&lt;/p&gt;

&lt;p&gt;Now, the math has changed. It is far more cost-effective to store petabytes of raw, unstructured log files in cheap object storage than to pay engineers to maintain fragile preprocessing scripts. If you decide next year that you need to analyze a nested JSON property you ignored today, the raw data is sitting there, waiting to be queried.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does ELT prevent permanent data loss during pipeline failures?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Under an ELT architecture, a buggy transformation query never results in data loss because the raw, unmodified data remains safely stored in the destination warehouse. If a transformation fails, you simply fix the SQL query and re-run it against the existing raw data. There is no need to ask upstream teams to replay events or re-send lost files.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine a scenario where your team is building a modern SaaS platform. You have a microservice emitting user interaction events. &lt;/p&gt;

&lt;p&gt;Under an ETL model, if your transformation server encounters an unexpected null value or an undocumented API schema change, the pipeline crashes. The data currently in flight can easily be corrupted or dropped before it ever reaches the database. &lt;/p&gt;

&lt;p&gt;With ELT, those raw event payloads are safely written directly to your database. Even if your downstream transformation queries fail and your analytics dashboard breaks, your source data is perfectly safe. You don't lose history; you just redeploy your transformation code, run the query again, and your dashboards recover instantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is ETL completely dead, or are there still valid use cases for it?
&lt;/h3&gt;

&lt;p&gt;ETL is still highly relevant when dealing with strict compliance and privacy laws. If your data contains sensitive PII (Personally Identifiable Information) that cannot legally be stored in its raw form on cloud servers, you must use ETL to mask, anonymize, or redact that data before loading it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does shifting to ELT significantly increase cloud compute costs?
&lt;/h3&gt;

&lt;p&gt;While ELT leverages the warehouse's compute engine to do heavy lifting, it is often more cost-effective than running dedicated, idle integration servers. Modern cloud databases optimize query execution plans and allow you to scale compute up or down dynamically, meaning you only pay for the exact seconds your transformations are running.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does ELT affect data governance and organization?
&lt;/h3&gt;

&lt;p&gt;Because ELT allows dumping raw data directly into the warehouse, it can lead to a "data swamp" if left unmanaged. Successful ELT implementations rely on clear schema separation—keeping a clean, untouched raw schema isolated from the clean, transformed production views that business intelligence tools query.&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>systemdesign</category>
      <category>cloudcomputing</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>How to Fix the Thundering Herd (Cache Stampede) Problem</title>
      <dc:creator>Doogal Simpson</dc:creator>
      <pubDate>Mon, 20 Jul 2026 12:38:24 +0000</pubDate>
      <link>https://dev.to/doogal/how-to-fix-the-thundering-herd-cache-stampede-problem-3men</link>
      <guid>https://dev.to/doogal/how-to-fix-the-thundering-herd-cache-stampede-problem-3men</guid>
      <description>&lt;p&gt;&lt;strong&gt;The thundering herd problem happens when a highly cached data item expires, forcing millions of concurrent requests to bypass the cache and slam your database at once. You can solve this by implementing cache locking (or single-flighting), which permits only the first request to rebuild the cache while holding the others in queue.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When I browse Reddit or a major news platform and suddenly get a 504 Gateway Timeout, my mind immediately jumps to system architecture. It is often not a coordinated DDoS attack causing the outage, but rather a self-inflicted system failure: the thundering herd problem. Let's look at how this failure mode works and how I approach fixing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the thundering herd problem?
&lt;/h2&gt;

&lt;p&gt;The thundering herd problem occurs when a heavily requested cache key expires, forcing all incoming traffic to query your database simultaneously. Instead of reading fast, temporary data from memory, millions of concurrent reads bypass the cache and hit the persistent storage layer all at once.&lt;/p&gt;

&lt;p&gt;I visualize this using a physical analogy. Imagine a stadium concourse as your cache and the database as a single turnstile gate. While the concourse is open, thousands of visitors move freely. But the moment that cache key expires, it is as if the main gate vanishes. &lt;/p&gt;

&lt;p&gt;The entire stadium crowd tries to squeeze through that single turnstile at the exact same millisecond. The database's CPU spikes to 100%, and it stops accepting any connections—taking the whole application down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does cache expiration cause database failure?
&lt;/h2&gt;

&lt;p&gt;When a cache key expires, the application registers a cache miss and routes the query to the database to rebuild the cache. If thousands of requests arrive during the fraction of a second it takes to fetch and write that data back, they will all hit the database at the same time.&lt;/p&gt;

&lt;p&gt;I always remind engineers that caches like Redis or Memcached serve data in microseconds because they operate in memory. Databases, being the disk-backed source of truth, take milliseconds to process queries. &lt;/p&gt;

&lt;p&gt;If you have a high-traffic microservice, even a tiny 50-millisecond window where the cache is empty is plenty of time for thousands of concurrent requests to pile up. The database gets overwhelmed trying to run the exact same heavy query thousands of times concurrently, resulting in connection pool exhaustion.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you solve the thundering herd problem?
&lt;/h2&gt;

&lt;p&gt;You solve this by implementing cache locking, which is also known as mutex locking or the single-flight pattern. When a cache miss occurs, the system locks that key so only the first request queries the database, while forcing all subsequent concurrent requests to wait until the cache is repopulated.&lt;/p&gt;

&lt;p&gt;Instead of letting every request hit the database, I establish a gatekeeper. The first request that encounters the expired cache grabs a lock, queries the database, writes the fresh data to the cache, and releases the lock. &lt;/p&gt;

&lt;p&gt;Meanwhile, the other thousands of pending requests wait for a few milliseconds. Once the lock is released, they are served directly from the newly updated cache without ever touching the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which tools support thundering herd protection?
&lt;/h2&gt;

&lt;p&gt;Many modern reverse proxies and caching layers have built-in directives designed to serialize cache-miss requests automatically. Configuring these native features is usually much safer and simpler than trying to write your own custom distributed locking mechanisms.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Configuration Strategy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NGINX&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;proxy_cache_use_stale&lt;/code&gt; / &lt;code&gt;proxy_cache_lock&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Serves stale cache data to incoming requests while a single backend request updates the cache.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Redis&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Distributed Locks (Redlock)&lt;/td&gt;
&lt;td&gt;Allows application nodes to synchronize cache population using lightweight keys.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Go (Singleflight)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sync.Mutex&lt;/code&gt; group&lt;/td&gt;
&lt;td&gt;Merges duplicate concurrent requests into a single execution shareable by all callers.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is the thundering herd problem the same as a cache stampede?
&lt;/h3&gt;

&lt;p&gt;Yes, "cache stampede" and "thundering herd" are different names for the same architectural failure mode where expired keys trigger an unmanageable wave of database queries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can we prevent this by using longer cache TTLs?
&lt;/h3&gt;

&lt;p&gt;No, increasing the TTL only reduces how often the event occurs; it does not solve the underlying vulnerability. When the key eventually expires under load, the system will still crash unless you have locking or background updates in place.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is background cache regeneration?
&lt;/h3&gt;

&lt;p&gt;It is an alternative strategy where background workers or cron jobs proactively update cache keys before they expire. Active traffic always reads from the warm cache and never triggers a synchronous database query on a miss.&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>caching</category>
      <category>softwareengineering</category>
      <category>backend</category>
    </item>
    <item>
      <title>Automate Video Editing with Whisper, LLMs, and FFmpeg</title>
      <dc:creator>Doogal Simpson</dc:creator>
      <pubDate>Sun, 19 Jul 2026 22:02:27 +0000</pubDate>
      <link>https://dev.to/doogal/automate-video-editing-with-whisper-llms-and-ffmpeg-2677</link>
      <guid>https://dev.to/doogal/automate-video-editing-with-whisper-llms-and-ffmpeg-2677</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR: To stop viewers from getting bored staring at my face during short technical videos, I built a system that parses my transcripts, generates relevant illustrations using AI, and splices them into the final video. This pipeline automates the visual storytelling, keeping things engaging without requiring manual editing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let’s be honest: staring at my face for 90 seconds while I explain abstract software architecture is a big ask. I get it. I’ve seen myself in the mirror. &lt;/p&gt;

&lt;p&gt;When we explain complex engineering ideas—like database sharding or async loops—visuals make everything click. But manually editing graphics into a timeline is incredibly tedious. To fix this, I built a system that listens to what I say, figures out what to show, generates the visuals, and drops them into the video.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why is manual video editing so inefficient for technical content?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Manual editing requires you to storyboard, design graphics, and line them up on a timeline just to explain a simple 90-second concept. Automating this process gets rid of the editing bottleneck entirely, letting you focus on the explanation while software handles the asset pipeline.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine you are explaining how a Redis cache sits in front of a database. In a traditional workflow, you write the script, record the video, open up an editor like Premiere or DaVinci Resolve, draw a diagram, and manually align it with your voice. &lt;/p&gt;

&lt;p&gt;Moving this logic into a software pipeline automates the tedious parts of video post-production. The code parses what is being said, determines where a visual cue should go, and handles the composition programmatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  How can you build an automated transcript-to-image pipeline?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You build this by parsing timestamps from your video transcript, passing those segments to an LLM to generate image prompts, generating the assets via an image API, and splicing them into the video using FFmpeg. This connects your spoken words directly to matching visuals.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is how the automated workflow breaks down from raw video to a visually augmented final cut:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Tools Used&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1. Parse Transcript&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Extract words and timestamp offsets from the audio&lt;/td&gt;
&lt;td&gt;Whisper API / SRT Parser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2. Identify Beats&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Find the moments that need visual explanation&lt;/td&gt;
&lt;td&gt;LLM API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3. Generate Visuals&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Turn the concept prompts into clean illustrations&lt;/td&gt;
&lt;td&gt;Stable Diffusion / DALL-E&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;4. Splicing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Overlay the generated images onto the video&lt;/td&gt;
&lt;td&gt;FFmpeg&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;By matching the transcript timestamps with our image generator, we can pinpoint exactly when a graphic needs to fade in and out without touching a timeline editor.&lt;/p&gt;




&lt;h2&gt;
  
  
  Does automated B-roll actually improve technical comprehension?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Automation speeds up production, but the real test is whether the generated visuals actually make the concepts easier to understand. If the images match the architecture you are describing, they help; if they are too random, they just become distracting.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine you are explaining how garbage collection works. A simple, timed graphic showing memory blocks being freed is miles ahead of watching a talking head. &lt;/p&gt;

&lt;p&gt;I want to find the sweet spot between automation and clarity. That is why I need your feedback: do these images actually help you grasp the concepts, or are they just making things more confusing? Drop a comment on my latest videos and let me know your thoughts.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What are the best tools for programmatically editing images into videos?
&lt;/h3&gt;

&lt;p&gt;FFmpeg is the industry standard for rendering, scaling, and overlaying images onto video streams via the command line. If you prefer working in Python, libraries like MoviePy offer simple wrappers over FFmpeg to handle the overlays.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you sync generated images with specific spoken words?
&lt;/h3&gt;

&lt;p&gt;By using speech-to-text engines like Whisper, you get timestamped JSON outputs for every word. The pipeline matches these timestamps with the generated image files to calculate the start frame and duration for the video overlay.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you prevent AI image generators from outputting gibberish text in diagrams?
&lt;/h3&gt;

&lt;p&gt;Tell the LLM in your prompt to generate abstract, non-textual diagrams and keep words out of the image entirely. Focus on shapes, flows, and clean icons rather than literal text labels.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>softwareengineering</category>
      <category>ai</category>
      <category>ffmpeg</category>
    </item>
    <item>
      <title>How Quorum Prevents Split-Brain in Distributed Systems</title>
      <dc:creator>Doogal Simpson</dc:creator>
      <pubDate>Sun, 19 Jul 2026 22:01:55 +0000</pubDate>
      <link>https://dev.to/doogal/how-quorum-prevents-split-brain-in-distributed-systems-51pn</link>
      <guid>https://dev.to/doogal/how-quorum-prevents-split-brain-in-distributed-systems-51pn</guid>
      <description>&lt;p&gt;&lt;strong&gt;Quick Answer:&lt;/strong&gt; A split-brain scenario occurs when a network partition isolates server nodes, leading multiple nodes to assume the "leader" role and accept conflicting writes. To prevent this data corruption, distributed systems enforce &lt;strong&gt;quorum&lt;/strong&gt;—requiring a strict majority of active nodes to approve any write operation, gracefully rejecting writes on isolated minority nodes.&lt;/p&gt;

&lt;p&gt;Imagine a cable gets cut between two servers. Both are still alive, but they can't talk to each other. What happens next? Left to their own devices, both servers assume the other has died, crown themselves the "master," and start accepting writes. This is the split-brain nightmare, and it's one of the fastest ways to corrupt your database.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a split-brain scenario in distributed systems?
&lt;/h2&gt;

&lt;p&gt;A split-brain scenario occurs when a network partition cuts off communication between nodes, causing isolated groups to independently elect their own master nodes. Because multiple masters now process writes simultaneously without coordination, it leads to conflicting data updates and severe state corruption.&lt;/p&gt;

&lt;p&gt;Let's look at a concrete example. Imagine we are building a distributed system to sell airline seats using two servers: Server A and Server B. Under normal conditions, they coordinate smoothly. But then, a network partition occurs.&lt;/p&gt;

&lt;p&gt;Server A can no longer communicate with Server B. It assumes Server B has crashed, so Server A declares itself the sole leader to keep the service running. Across the network split, Server B goes through the exact same thought process, assuming Server A has died, and also promotes itself to master.&lt;/p&gt;

&lt;p&gt;Now, you have two master nodes operating independently. If two different users concurrently try to book seat 12A—one hitting Server A and the other hitting Server B—both servers will accept the write. Once the network heals and the servers attempt to sync, you are left with a double-booked seat and corrupted, irreconcilable data.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does quorum prevent split-brain data corruption?
&lt;/h2&gt;

&lt;p&gt;Quorum prevents split-brain by requiring that any write operation or leader election must be approved by a strict majority of the total nodes in the cluster. If a network partition occurs, the sub-cluster containing the minority of nodes will fail to reach this threshold and gracefully reject write requests, preventing conflicting state updates.&lt;/p&gt;

&lt;p&gt;Instead of relying on a fragile two-node setup, distributed systems use an odd number of nodes to establish a majority rule. For any cluster of N nodes, a quorum requires at least floor(N/2) + 1 active nodes to agree before any write is committed.&lt;/p&gt;

&lt;p&gt;Let's scale our ticketing system up to 9 nodes. If a network partition occurs, splitting the cluster into one group of 5 nodes and another group of 4 nodes, the quorum math protects us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The majority partition (5 nodes):&lt;/strong&gt; It can still reach the required quorum of 5 nodes (since 5 out of 9 are present). This side continues to accept writes normally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The minority partition (4 nodes):&lt;/strong&gt; If a write request hits this side, the nodes try to coordinate but realize they can only get 4 votes. Since 4 is less than the required 5, they reject the write.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The table below highlights how clusters behave during a partition depending on whether quorum is enforced:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attribute&lt;/th&gt;
&lt;th&gt;Without Quorum (Split-Brain)&lt;/th&gt;
&lt;th&gt;With Quorum (Majority Rule)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Active Leaders&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Multiple active leaders (one per isolated partition)&lt;/td&gt;
&lt;td&gt;Exactly one leader (only in the majority partition)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Consistency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High risk of split-brain corruption and lost writes&lt;/td&gt;
&lt;td&gt;Guaranteed consistency; conflicting writes are blocked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Minority Partition Behavior&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Accepts uncoordinated writes, leading to state drift&lt;/td&gt;
&lt;td&gt;Rejects writes or falls back to safe read-only mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Recovery Process&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Complex, manual, and painful database reconciliation&lt;/td&gt;
&lt;td&gt;Automatic; minority nodes catch up once the network heals&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why is failing gracefully better than accepting every write?
&lt;/h2&gt;

&lt;p&gt;Failing gracefully protects the integrity of your system's state, adhering to the CAP theorem's preference for consistency over availability during a partition. Accepting unverified writes during a network split guarantees data corruption, which is significantly harder and more expensive to recover from than a temporary, localized outage.&lt;/p&gt;

&lt;p&gt;As software engineers, we often obsess over 100% uptime. However, there is a massive difference between a system that is temporarily unavailable and a system that actively writes bad data. &lt;/p&gt;

&lt;p&gt;If your system rejects a booking because it cannot reach a quorum, you might lose a transaction or frustrate a user in the short term. But if your system accepts the booking on both sides of a partition, you now have two customers showing up for the exact same physical seat. Reconciling corrupted data post-incident requires complex compensation logic, manual support intervention, and ruins user trust. Using quorum forces the minority partition to fail gracefully, keeping your data source of truth pristine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What happens if a cluster splits into two equal halves?
&lt;/h3&gt;

&lt;p&gt;If a cluster splits into two equal halves (for example, a 4-node cluster splitting into groups of 2 and 2), neither side can achieve a strict majority of 3 nodes. Consequently, both sides fail to achieve quorum, and the entire cluster stops accepting writes until connectivity is restored. This is why distributed databases are almost always deployed with an odd number of nodes (3, 5, or 7).&lt;/p&gt;

&lt;h3&gt;
  
  
  How do consensus protocols like Raft and Paxos leverage quorum?
&lt;/h3&gt;

&lt;p&gt;Consensus protocols like Raft and Paxos use quorum to ensure a single, consistent state across the cluster. During a leader election, a candidate node must win votes from a majority of the cluster to become the leader. Similarly, when writing data, the leader must replicate the log entry to a majority of nodes before committing the write, ensuring that a minority partition can never overwrite committed data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can a minority partition still serve read requests?
&lt;/h3&gt;

&lt;p&gt;Yes, depending on how you configure your database's consistency model. Many distributed databases allow the minority partition to serve "stale" or eventually consistent reads, even if they reject writes. However, if your application requires strict linearizable reads (ensuring you always read the absolute latest write), even read requests must query a quorum of nodes, meaning the minority partition would have to reject reads as well.&lt;/p&gt;

</description>
      <category>distributedsystems</category>
      <category>systemdesign</category>
      <category>database</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Why You Should Never Use a Database as an API</title>
      <dc:creator>Doogal Simpson</dc:creator>
      <pubDate>Sun, 19 Jul 2026 22:01:38 +0000</pubDate>
      <link>https://dev.to/doogal/why-you-should-never-use-a-database-as-an-api-4nfg</link>
      <guid>https://dev.to/doogal/why-you-should-never-use-a-database-as-an-api-4nfg</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR: Sharing databases across team boundaries bypasses service interfaces, turning database schemas into public APIs. This tightly couples services, halts schema migrations, and stalls development. To maintain architectural agility, I always advise teams to hide their databases behind well-defined APIs, decoupling internal implementation from external integration.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'm no big fan of Jeff Bezos, but his early Amazon API mandate was absolute gold. The story goes that he sent a company-wide email warning that if anyone used another team's database directly as an API, they would be fired. While threatening to sack your engineering team is a bit extreme, the technical wisdom behind it is something I advocate for constantly. I see teams take this shortcut all the time, only to watch it blow up in their faces later. Here is my breakdown of why we need to stop doing this and how we can build better boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is direct database sharing bad for microservices?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;In my experience, sharing databases directly across services destroys boundaries because it turns your internal storage schema into your public interface. When external teams query your database directly, you lose the ability to rename columns, change data types, or refactor your schema without breaking their systems.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I like to think of this using a physical analogy. Imagine you are setting up a smart home. If your smart lights plug into standard wall outlets, they work perfectly because they rely on a stable, standardized interface. But if those lights required you to splice raw copper wires directly into your home's main electrical panel, you could never upgrade your wiring or swap a circuit breaker without risking a blackout.&lt;/p&gt;

&lt;p&gt;When Team B queries Team A's database directly, they bypass the API layer. The database schema—the raw copper wire—becomes the interface. If Team A wants to rename a column from &lt;code&gt;user_id&lt;/code&gt; to &lt;code&gt;account_id&lt;/code&gt; or migrate from PostgreSQL to MongoDB, they are stuck. They cannot make changes because they no longer own their data model.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do API interfaces solve the database coupling problem?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;From what I've seen, APIs solve the coupling problem by introducing a layer of abstraction between how data is stored and how it is consumed. By exposing data through HTTP endpoints, gRPC, or event streams, the owning team can freely modify their database schema as long as the API payload remains contract-compliant.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's say you have a billing service that needs data from a user profile service. Instead of writing a direct SQL query against the &lt;code&gt;users&lt;/code&gt; table, the billing service should call &lt;code&gt;GET /users/{id}&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Behind that HTTP endpoint, the profile service can restructure its tables, split a single table into three normalized tables, or even change its caching strategy. As long as the JSON response format does not break, the billing service does not know and does not care. This separation of concerns keeps teams operating independently and moving fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are the trade-offs of direct database access versus API integration?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;I always warn developers that while direct database access offers lower initial latency and faster setup, it leads to tight coupling and unmaintainable technical debt. API integration requires upfront design and introduces minor network overhead but ensures team autonomy, independent scaling, and safe schema evolution.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric / Feature&lt;/th&gt;
&lt;th&gt;Direct Database Access&lt;/th&gt;
&lt;th&gt;Service Interface (API)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Coupling Level&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Extremely Tight (Schema is public)&lt;/td&gt;
&lt;td&gt;Loose (Schema is private)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Development Speed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fast initially, degrades rapidly over time&lt;/td&gt;
&lt;td&gt;Consistent, predictable long-term velocity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Schema Migrations&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High-risk (will break external consumers)&lt;/td&gt;
&lt;td&gt;Safe (hidden behind abstraction layer)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Security &amp;amp; Auditing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Difficult (requires database-level permissions)&lt;/td&gt;
&lt;td&gt;Simple (handled at the application layer)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Technology Choice&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Locked into a single database technology&lt;/td&gt;
&lt;td&gt;Free to choose different DBs per service&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How do we transition away from shared databases?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;To transition away from a shared database model, I recommend establishing clear data ownership and creating wrappers around legacy data paths. Teams must stop writing cross-schema joins, build lightweight API wrappers over the data, and migrate consumers to the new endpoints.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you are currently stuck in a "shared database culture," I do not expect you to rewrite everything overnight. Start by treating the legacy tables as deprecated contracts. Write a thin service layer that queries those tables and exposes the data via standard HTTP endpoints. Have other teams transition their read queries to these new endpoints one by one. Once the direct database queries drop to zero, you finally own your schema again and can begin refactoring with confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is sharing a database schema ever acceptable?
&lt;/h3&gt;

&lt;p&gt;I only allow it within the boundary of a single, deployable microservice owned by one team. If multiple teams or services deploy independently but touch the same database schema, you have a distributed monolith, not microservices.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does this rule apply to read-only database replicas?
&lt;/h3&gt;

&lt;p&gt;Absolutely. Even if another team is only reading from a read replica of your database to avoid performance degradation, they are still coupled to your underlying schema. If you rename a column on the primary database, the replica schema changes and their queries will fail.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do we handle high-performance reporting without direct database access?
&lt;/h3&gt;

&lt;p&gt;Instead of querying another team's database directly for reporting, I recommend using asynchronous event streaming (like Kafka or RabbitMQ) to replicate the necessary data to a dedicated reporting database, or utilizing an ETL pipeline to move data to a central data warehouse.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>systemdesign</category>
      <category>api</category>
      <category>microservices</category>
    </item>
    <item>
      <title>Should You Use Custom ID Types or Plain Strings?</title>
      <dc:creator>Doogal Simpson</dc:creator>
      <pubDate>Sun, 19 Jul 2026 22:00:26 +0000</pubDate>
      <link>https://dev.to/doogal/should-you-use-custom-id-types-or-plain-strings-1do5</link>
      <guid>https://dev.to/doogal/should-you-use-custom-id-types-or-plain-strings-1do5</guid>
      <description>&lt;p&gt;&lt;strong&gt;Should you use a custom ID type or just stick to strings? Sticking to raw strings is simpler and faster to write, but wrapping your IDs in a custom domain type prevents developer mistakes—like running substring or split operations on a UUID. The right choice depends on your team's preference for speed versus absolute correctness.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I have lost count of the times I have seen developers treat IDs as generic strings, only for it to backfire. Imagine debugging a production crash only to find someone ran a &lt;code&gt;.split('-')&lt;/code&gt; on a database UUID to "extract a prefix" for a lookup. It sounds absurd, but when IDs are passed around as raw strings, they inherit all of string's behaviors. I have noticed that developers have incredibly strong opinions on this, so I want to talk about the age-old debate: primitive strings versus custom ID types.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is using primitive strings for database IDs considered bad practice?
&lt;/h2&gt;

&lt;p&gt;I find that using raw strings for database IDs is risky because it exposes a massive API surface of string manipulation methods that make no sense for an identifier. It allows developers to treat a unique domain identifier as arbitrary text, which often leads to silent bugs and loose domain boundaries.&lt;/p&gt;

&lt;p&gt;In my view, an ID exists for one primary reason: equality comparison. You want to know if &lt;code&gt;RecordA.id === RecordB.id&lt;/code&gt;. You never need to pad an ID, slice it, or regex-match it in normal business logic. &lt;/p&gt;

&lt;p&gt;Think of an ID like a physical key to a house. You do not slice a physical key in half to see if it fits a smaller lock; you either use it to open the door, or you do not. When we type an ID as a string, I feel like we are giving developers a Swiss Army knife when they only needed a key.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you implement a custom ID type in code?
&lt;/h2&gt;

&lt;p&gt;When I implement a custom ID type, I wrap the primitive value inside a dedicated class or value object. This encapsulates the internal representation and restricts the available operations solely to creation and equality checks.&lt;/p&gt;

&lt;p&gt;Here is a lightweight example of how I like to wrap a primitive string ID in TypeScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="nf"&gt;equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;other&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What are the trade-offs of custom ID types vs primitive strings?
&lt;/h2&gt;

&lt;p&gt;From what I have seen, this trade-off is a classic battle between cognitive overhead and runtime safety. While custom ID types prevent illegal operations and accidental type mixing, they require extra boilerplate, serialization handling, and onboarding effort for new team members.&lt;/p&gt;

&lt;p&gt;Here is how I break down the architectural and developer experience impacts of both approaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Raw Strings (The Fast Path)&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;em&gt;Pros&lt;/em&gt;: Zero boilerplate; works out of the box with JSON serialization and ORMs; lower cognitive load.&lt;/li&gt;
&lt;li&gt;  &lt;em&gt;Cons&lt;/em&gt;: Allows nonsensical operations (like &lt;code&gt;id.split()&lt;/code&gt;); makes it easy to accidentally pass a &lt;code&gt;ProductId&lt;/code&gt; into a function expecting a &lt;code&gt;CustomerId&lt;/code&gt; because both are just strings.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Custom ID Types (The Safe Path)&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;em&gt;Pros&lt;/em&gt;: Prevents domain errors at compile-time; restricts API surface to equality checks only; highly self-documenting.&lt;/li&gt;
&lt;li&gt;  &lt;em&gt;Cons&lt;/em&gt;: Requires custom serializers for database and API boundaries; adds slight friction when onboarding new developers.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How should a team decide between custom IDs and strings?
&lt;/h2&gt;

&lt;p&gt;I always advise teams to choose based on their operational culture and domain complexity. Fast-moving startups or prototyping teams usually prefer the speed of raw strings, while teams building long-lived, high-stakes systems benefit heavily from the strict correctness of custom types.&lt;/p&gt;

&lt;p&gt;Ultimately, I do not think either approach is objectively wrong. If your team culture values shipping fast and trusts team discipline to keep things clean, strings are fine. But if you are building a system where a mixed-up ID could mean transferring money to the wrong account, I believe that compile-time safety is worth every line of boilerplate.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does using a custom ID type hurt database query performance?
&lt;/h3&gt;

&lt;p&gt;No, it does not. Custom ID types only exist at the application layer for compile-time or runtime safety. Once the data is compiled down or serialized, it is sent to the database as a standard primitive (like a string or binary UUID), leaving query performance completely unaffected.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you handle JSON serialization with custom ID types?
&lt;/h3&gt;

&lt;p&gt;Most modern languages and frameworks allow you to write custom serialization adapters. For instance, I configure my JSON parser to automatically serialize the custom type into its raw string value when sending payloads over the network, and deserialize it back into the class upon receipt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can TypeScript's 'branding' feature solve this without classes?
&lt;/h3&gt;

&lt;p&gt;Yes, TypeScript allows you to use "branded types" (or nominal typing) to differentiate strings at compile time without creating runtime classes. This gives you type safety preventing you from passing a CustomerId to a ProductId parameter, while keeping the runtime value a plain string.&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>cleancode</category>
      <category>typesafety</category>
      <category>programming</category>
    </item>
    <item>
      <title>When to Use an ORM vs SQL and Query Builders</title>
      <dc:creator>Doogal Simpson</dc:creator>
      <pubDate>Sun, 19 Jul 2026 22:00:09 +0000</pubDate>
      <link>https://dev.to/doogal/when-to-use-an-orm-vs-sql-and-query-builders-1k</link>
      <guid>https://dev.to/doogal/when-to-use-an-orm-vs-sql-and-query-builders-1k</guid>
      <description>&lt;p&gt;&lt;strong&gt;ORMs like Prisma and Hibernate are excellent for simplifying basic CRUD operations. However, for complex queries involving window functions, CTEs, or deep joins, ORMs often get in the way. For advanced use cases, learning SQL first and using a modern query builder like Drizzle offers the perfect balance of static typing and control.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We have all been there. You start a new project, pull in a heavy Object-Relational Mapper (ORM) to make database interactions "easier," and everything goes smoothly while you are just writing basic endpoints. But the moment you need to fetch nested data with a complex join, a window function, or a common table expression (CTE), that helpful abstraction suddenly feels like a brick wall. You end up spending more time fighting the ORM's custom syntax than actually writing the query.&lt;/p&gt;

&lt;p&gt;Here is how I approach the decision of when to use an ORM, when to drop down to a query builder, and why learning SQL first is non-negotiable.&lt;/p&gt;

&lt;h2&gt;
  
  
  When is an ORM actually useful?
&lt;/h2&gt;

&lt;p&gt;ORMs are highly effective when your application primarily performs standard Create, Read, Update, and Delete (CRUD) operations. They eliminate repetitive boilerplate code by letting you interact with database rows as native programming language objects. &lt;/p&gt;

&lt;p&gt;If you are building an application where you just need to get a record by ID, update a single column, or fetch list data based on simple filters, an ORM is a massive time-saver. It handles the mapping automatically, keeps your database schema in sync with your application models, and lets you move incredibly fast during the early stages of development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do ORMs fail on complex queries?
&lt;/h2&gt;

&lt;p&gt;ORMs struggle with complex queries because their object-oriented abstractions do not map cleanly to relational algebra operations like window functions or common table expressions (CTEs). Trying to force these operations through an ORM wrapper often leads to unoptimized database calls or incredibly convoluted code.&lt;/p&gt;

&lt;p&gt;Imagine you are building an analytics dashboard that needs to calculate a running total of transactions grouped by category. In SQL, this is a straightforward window function. In a traditional ORM, you either have to write a highly complex, unreadable method chain, or worse, retrieve thousands of rows into your application memory to calculate the sum in code. When the abstraction gets in the way of writing clean, performant queries, it ceases to be useful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comparing Database Access Methods
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Database Task&lt;/th&gt;
&lt;th&gt;ORMs (e.g., Prisma / Hibernate)&lt;/th&gt;
&lt;th&gt;Query Builders (e.g., Drizzle)&lt;/th&gt;
&lt;th&gt;Raw SQL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Basic CRUD&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Excellent (Fastest setup)&lt;/td&gt;
&lt;td&gt;Good (Type-safe but manual)&lt;/td&gt;
&lt;td&gt;Tedious (High boilerplate)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Complex Joins &amp;amp; CTEs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Poor (Clunky API)&lt;/td&gt;
&lt;td&gt;Excellent (Matches SQL layout)&lt;/td&gt;
&lt;td&gt;Perfect (Full control)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Performance Overhead&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High (Heavy abstraction)&lt;/td&gt;
&lt;td&gt;Minimal (Thin wrapper)&lt;/td&gt;
&lt;td&gt;Zero (Direct execution)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Should you learn SQL before learning an ORM?
&lt;/h2&gt;

&lt;p&gt;Yes, you should absolutely master raw SQL before relying on an ORM. Understanding how relational databases handle queries under the hood gives you the mental model needed to write performant database interactions and debug ORM-generated queries when they fail.&lt;/p&gt;

&lt;p&gt;If you are new to software engineering, learning how an ORM works before understanding SQL is a trap. ORM libraries and frameworks go out of style, but SQL is an industry standard that has persisted for decades. When you understand SQL, you have total control; you know exactly what your database is doing, and you can easily evaluate whether an ORM is helping or hurting your application's performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the best alternative to a heavy ORM?
&lt;/h2&gt;

&lt;p&gt;A lightweight query builder is the ideal middle ground between raw SQL and a heavy ORM. Tools like Drizzle in the Node.js ecosystem allow you to write type-safe queries that closely resemble real SQL, giving you full control without the overhead of an object-relational mapping layer.&lt;/p&gt;

&lt;p&gt;Instead of writing a raw string of SQL that your editor cannot validate, query builders allow you to use static typing to catch errors at compile time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transactions&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;db&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transactionsTable&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="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transactionsTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transactionsTable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transactionsTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transactionsTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach gives you the predictability of SQL with the developer experience and safety of modern TypeScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can you mix raw SQL with an ORM?
&lt;/h3&gt;

&lt;p&gt;Yes. Most modern ORMs provide an "escape hatch" that allows you to write raw SQL queries for complex operations while still using the ORM for standard CRUD tasks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do ORMs hurt database performance?
&lt;/h3&gt;

&lt;p&gt;They can. ORMs often generate inefficient SQL queries (such as the N+1 query problem) or fetch more columns than necessary because they default to selecting entire rows rather than specific fields.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between an ORM and a query builder?
&lt;/h3&gt;

&lt;p&gt;An ORM maps database tables directly to application objects and manages relations automatically, whereas a query builder simply provides a programmatic, type-safe way to construct raw SQL queries.&lt;/p&gt;

</description>
      <category>database</category>
      <category>sql</category>
      <category>webdev</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Solving the N+1 Query Problem in Databases and ORMs</title>
      <dc:creator>Doogal Simpson</dc:creator>
      <pubDate>Sun, 19 Jul 2026 21:59:52 +0000</pubDate>
      <link>https://dev.to/doogal/solving-the-n1-query-problem-in-databases-and-orms-ifb</link>
      <guid>https://dev.to/doogal/solving-the-n1-query-problem-in-databases-and-orms-ifb</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR: The N+1 query problem occurs when an ORM executes one initial database query followed by N subsequent queries to fetch related child data. I solve this by using SQL joins, batch fetching (IN queries), or data denormalization. This guide shows you how to implement each fix.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let’s be honest: ORMs are a double-edged sword. Tools like Hibernate, Prisma, or TypeORM sucker you in with incredibly slick, beautiful interfaces. They make database interactions feel like pure poetry—until they turn around and beat you down with nasty performance bugs. &lt;/p&gt;

&lt;p&gt;I see this happen all the time. You build a clean-looking feature, push it to production, and suddenly your database performance falls off a cliff. Nine times out of ten, you’ve quietly introduced the classic N+1 query problem without even realizing it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is the N+1 query problem?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;To put it simply, the N+1 query problem is what happens when your application makes one query to fetch parent records, and then makes N individual queries to fetch related child data. Instead of hitting the database once, you end up hitting it N+1 times. If you have 100 records, you are making 101 queries, which will absolutely melt your database under load.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's look at a hypothetical scenario. Imagine your team is building a social media feed. You want to display a list of posts, and each post needs to show the author’s username. &lt;/p&gt;

&lt;p&gt;Under the hood, your ORM might make one query to grab the posts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, to display the usernames, it loops through those posts and fires off a separate query for every single post:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suddenly, what should have been a single fast database roundtrip turns into a barrage of 101 queries. I promise you, doing this repeatedly when users load their feeds is a fast track to a production outage.&lt;/p&gt;




&lt;h2&gt;
  
  
  How do you identify N+1 queries in your code?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;I find the easiest way to catch N+1 queries is by enabling SQL logging in development or using APM tools to monitor your database traffic. If you see a massive block of near-identical SELECT statements executing in rapid succession, you have an N+1 bug. You can also spot them by keeping a close eye on your ORM’s relationship loading behavior.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you do not have APM tools set up, just watch your console output in your local dev environment. When a single page load triggers a continuous scroll of database logs that look identical except for the primary key, I guarantee you have a performance problem on your hands.&lt;/p&gt;




&lt;h2&gt;
  
  
  What are the best ways to solve the N+1 query problem?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;I resolve N+1 queries using one of three main strategies: database JOINs, batch queries with an IN clause, or data denormalization. The right choice depends on your read-to-write ratios and how complex your relationships are. Here is how I break down these three approaches:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;th&gt;Database Roundtrips&lt;/th&gt;
&lt;th&gt;Complexity&lt;/th&gt;
&lt;th&gt;When I Use It&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SQL JOIN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Standard relational databases where tables are indexed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Batching (IN clause)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Large datasets where large JOINs become too expensive.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Denormalization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Scale-heavy, read-intensive feeds where performance is everything.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  How does a SQL JOIN solve N+1?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;A SQL JOIN combines both tables into a single query, which allows the database engine to correlate and return all your data in one roundtrip. Instead of retrieving raw posts and then looking up users, you return an "enhanced" post that already contains the username details. This is usually my go-to fix because it leverages what SQL databases do best.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most modern ORMs allow you to trigger this behavior using eager loading flags. You just tell the ORM to explicitly include the relation, and it will rewrite the underlying query to use a join under the hood.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does batching with an IN clause fix the issue?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Batch fetching solves the problem in exactly two database queries by utilizing a SQL IN clause to retrieve child data in bulk. First, the application fetches the parent records, extracts the foreign keys, and then runs a single query to get all related rows at once. This keeps the total database roundtrips to two, regardless of how many records you have.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In our feed scenario, this means you run one query for the 100 posts, extract all the &lt;code&gt;userId&lt;/code&gt; values, and run a single follow-up: &lt;code&gt;SELECT * FROM users WHERE id IN (1, 2, 3...)&lt;/code&gt;. It is incredibly clean and prevents massive, slow joins on huge datasets.&lt;/p&gt;

&lt;h3&gt;
  
  
  When should you resort to data denormalization?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Data denormalization is the right choice when you are working with non-relational structures or read-heavy systems where you simply cannot afford joins. You solve N+1 by writing the duplicate data—like storing the username directly inside the posts table—so everything you need is in one place. Just remember that you will have the persistent headache of keeping that duplicated data updated when a user changes their username.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If a user updates their name, you have to run a background job to cascade that update to every single post they've ever written. It's a trade-off: you get lightning-fast reads, but your write logic gets significantly more complex.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why do ORMs default to lazy loading if it is so dangerous?
&lt;/h3&gt;

&lt;p&gt;Lazy loading is the default because it is convenient and saves memory by only fetching what you ask for. But if you ask me, it is a total trap; it assumes developers will manually specify eager loading whenever they handle collections, which we almost always forget to do.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does eager loading completely eliminate the N+1 query problem?
&lt;/h3&gt;

&lt;p&gt;Yes, eager loading forces the ORM to fetch related data upfront, usually through a JOIN or an IN query. However, you have to be careful not to eager-load every single relation on a model, or you will end up pulling half your database into application memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you handle denormalized data sync issues?
&lt;/h3&gt;

&lt;p&gt;If you go the denormalization route, you have to accept the trade-off of maintaining that data. I usually handle this by triggering asynchronous background workers or event listeners to update the duplicated username across all post records whenever a user changes their name.&lt;/p&gt;

</description>
      <category>database</category>
      <category>sql</category>
      <category>softwareengineering</category>
      <category>databaseperformance</category>
    </item>
    <item>
      <title>How Collaborative Docs Work: An Introduction to CRDTs</title>
      <dc:creator>Doogal Simpson</dc:creator>
      <pubDate>Sun, 19 Jul 2026 21:59:35 +0000</pubDate>
      <link>https://dev.to/doogal/how-collaborative-docs-work-an-introduction-to-crdts-26ga</link>
      <guid>https://dev.to/doogal/how-collaborative-docs-work-an-introduction-to-crdts-26ga</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR: Collaborative documents avoid synchronization conflicts by using Conflict-free Replicated Data Types (CRDTs) instead of index-based positioning. By assigning every character a unique, immutable identifier and referencing edits to those IDs rather than numeric array indexes, concurrent changes merge predictably regardless of the order they arrive at the server.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you have ever worked on a shared document in Google Docs or Notion, you have probably taken real-time collaboration for granted. But under the hood, syncing text across multiple distributed clients is a notoriously difficult problem. I want to dive into why naive solutions break, and explain how I think about resolving this conflict using an elegant data structure called a CRDT.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does index-based positioning fail in collaborative editing?
&lt;/h2&gt;

&lt;p&gt;Index-based positioning fails because concurrent edits shift the character offsets of the document in real time. When multiple users send edits based on their local view of the document, the absolute indexes become desynchronized, leading to corrupt text when those edits are merged on other clients.&lt;/p&gt;

&lt;p&gt;Imagine you are building a collaborative text editor. The document currently contains the word &lt;code&gt;cat&lt;/code&gt;. Two users try to edit it simultaneously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User 1&lt;/strong&gt; wants to change it to &lt;code&gt;chat&lt;/code&gt; by adding an &lt;code&gt;h&lt;/code&gt; at index 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User 2&lt;/strong&gt; wants to change it to &lt;code&gt;cats&lt;/code&gt; by adding an &lt;code&gt;s&lt;/code&gt; at index 3.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you naively apply these edits using array indexes, the final state depends entirely on which edit runs first. &lt;/p&gt;

&lt;p&gt;If User 1's edit runs first, the word becomes &lt;code&gt;chat&lt;/code&gt;. When you then apply User 2's edit (insert &lt;code&gt;s&lt;/code&gt; at index 3), you end up with &lt;code&gt;chast&lt;/code&gt;—which is completely wrong. Conversely, if User 2's edit runs first, you get &lt;code&gt;cats&lt;/code&gt;, and applying User 1's edit results in &lt;code&gt;chats&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Because the order of network packets dictates the final text, clients will end up with mismatched documents. &lt;/p&gt;

&lt;h2&gt;
  
  
  How do CRDTs solve the collaborative text sync problem?
&lt;/h2&gt;

&lt;p&gt;Conflict-free Replicated Data Types (CRDTs) solve this by assigning a globally unique, immutable identifier to every single character in a document. Edits are then declared relative to these static identifiers rather than volatile numeric indexes, ensuring that operations can be applied in any order with the same final state.&lt;/p&gt;

&lt;p&gt;To solve this, I prefer to model the document not as a raw array of characters, but as a collection of metadata nodes. Every character node is assigned a unique ID consisting of the user's ID and a local counter.&lt;/p&gt;

&lt;p&gt;For example, I would structure the word &lt;code&gt;cat&lt;/code&gt; like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;c&lt;/code&gt; -&amp;gt; ID: &lt;code&gt;user0v1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;a&lt;/code&gt; -&amp;gt; ID: &lt;code&gt;user0v2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;t&lt;/code&gt; -&amp;gt; ID: &lt;code&gt;user0v3&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When User 1 wants to insert &lt;code&gt;h&lt;/code&gt;, they do not say "insert at index 1." Instead, they say "insert &lt;code&gt;h&lt;/code&gt; (ID: &lt;code&gt;user1v1&lt;/code&gt;) directly &lt;em&gt;after&lt;/em&gt; &lt;code&gt;user0v1&lt;/code&gt;." At the same time, User 2 inserts &lt;code&gt;s&lt;/code&gt; (ID: &lt;code&gt;user2v1&lt;/code&gt;) directly &lt;em&gt;after&lt;/em&gt; &lt;code&gt;user0v3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is how I represent that metadata under the hood for a single edit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user1v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"char"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"h"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"after"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user0v1"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This representation maps out how every character relates to its neighbors:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Character&lt;/th&gt;
&lt;th&gt;Unique ID&lt;/th&gt;
&lt;th&gt;Inserted After ID&lt;/th&gt;
&lt;th&gt;Final Position&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;c&lt;/td&gt;
&lt;td&gt;&lt;code&gt;user0v1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start&lt;/td&gt;
&lt;td&gt;1st&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;h&lt;/td&gt;
&lt;td&gt;&lt;code&gt;user1v1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;user0v1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2nd&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a&lt;/td&gt;
&lt;td&gt;&lt;code&gt;user0v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;user0v1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3rd&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t&lt;/td&gt;
&lt;td&gt;&lt;code&gt;user0v3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;user0v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4th&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s&lt;/td&gt;
&lt;td&gt;&lt;code&gt;user2v1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;user0v3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5th&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why is the order of applied edits irrelevant in a CRDT?
&lt;/h2&gt;

&lt;p&gt;CRDTs are mathematically designed to be commutative and associative, meaning the order in which network packets arrive does not affect the final computed state. Because each insertion or deletion operation refers to static character IDs, the resulting document structure converges to the exact same state on all clients.&lt;/p&gt;

&lt;p&gt;No matter what order the network delivers the packets, the relationship remains the same. Whether the local client processes the &lt;code&gt;h&lt;/code&gt; insertion or the &lt;code&gt;s&lt;/code&gt; insertion first, &lt;code&gt;h&lt;/code&gt; will always sit after &lt;code&gt;c&lt;/code&gt; (&lt;code&gt;user0v1&lt;/code&gt;), and &lt;code&gt;s&lt;/code&gt; will always sit after &lt;code&gt;t&lt;/code&gt; (&lt;code&gt;user0v3&lt;/code&gt;). I find this mathematical certainty incredibly satisfying because it guarantees that every client eventually converges on the exact same word: &lt;code&gt;chats&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do CRDTs handle character deletions?
&lt;/h3&gt;

&lt;p&gt;To delete a character, CRDTs typically use "tombstones." Instead of completely removing the node from the tree (which would break other pending edits referencing its ID), the node is marked as invisible. This preserves the document's relational structure while hiding the character from the user.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between Operational Transformation (OT) and CRDTs?
&lt;/h3&gt;

&lt;p&gt;Operational Transformation (OT) relies on a central server to rewrite the indexes of incoming edits before broadcasting them to other clients. CRDTs are peer-to-peer friendly; they do not require a central coordinating server to resolve conflicts because the data structures resolve themselves mathematically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do CRDTs make document file sizes too bloated?
&lt;/h3&gt;

&lt;p&gt;Yes, CRDTs introduce metadata overhead because every single character requires an ID and positioning pointers. However, modern CRDT implementations use optimizations like run-length encoding and state-tree pruning to keep memory usage highly performant.&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>distributedsystems</category>
      <category>softwareengineering</category>
      <category>crdt</category>
    </item>
  </channel>
</rss>
