<?xml version="1.0"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>/dev/solita</title>
    <link>http://dev.solita.fi</link>
    <atom:link type="application/rss+xml" rel="self" href="http://dev.solita.fi/rss.xml" />
    <description>Solita developer blog</description>
    <language>en-us</language>
    <pubDate>Mon, 11 May 2026 10:06:31 +0000</pubDate>
    <lastBuildDate>Mon, 11 May 2026 10:06:31 +0000</lastBuildDate>

    
    <item>
      <title>What I Learned from Porting an Astro HTML Generator to Java with AI</title>
      <link>http://dev.solita.fi/2026/05/11/what-i-learned-from-porting-code-with-ai.html</link>
      <pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate>
      <author>open@solita.fi (Solita Oy)</author>
      <guid>http://dev.solita.fi/2026/05/11/what-i-learned-from-porting-code-with-ai</guid>
	  
      <description>&lt;p&gt;There have probably been many situations where software development teams have made the “wrong” technology choice but couldn’t justify switching because they were already too deep in the swamp. Maybe the chosen technology turned out to be hard to maintain, support got dropped mid-project or the choice was simply a poor fit for the problem — but since it works, rewriting it could not be justified and developers just have to live with their choice.&lt;/p&gt;

&lt;p&gt;You might remember from my &lt;a href=&quot;https://dev.solita.fi/2024/12/02/building-static-websites-with-astro.html&quot;&gt;earlier blog post&lt;/a&gt; that we picked &lt;a href=&quot;https://astro.build/&quot;&gt;Astro&lt;/a&gt;, a Node-based framework, for our static website generator project. Astro was chosen because it felt modern, content-focused, and was easy to pick up for any React developer — just what we needed. While the previous blog post is still relevant and I personally enjoyed using Astro, it ended up being the wrong choice in our project. The main reason was performance: we have strict requirements for site generation speed that Astro just couldn’t meet despite multiple optimisation attempts. This created a motivation for us to begin thinking about switching it to something else.&lt;/p&gt;

&lt;p&gt;Switching technologies is a significant change that would traditionally require a lot of work. With today’s AI agents, however, the change is possible to carry out in significantly less time, leveraging existing code as much as possible. For AI, an existing codebase with good tests is like precisely written documentation of how a system should work. And since AI has proven to be very efficient in generating working systems from text prompts, it can use the existing implementation as a “prompt” for generating a similar system in another technology.&lt;/p&gt;

&lt;h2 id=&quot;why-astro-wasnt-for-us&quot;&gt;Why Astro Wasn’t For Us&lt;/h2&gt;

&lt;p&gt;Our practical experience of using Astro for two years revealed that the generation slowness was mainly caused by two factors. First, Astro orchestrates page generation by itself (via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getStaticPaths&lt;/code&gt;) and doesn’t really allow fine-grained optimization such as parallel page generation or reading all required data into memory once before generation starts. Second, while splitting HTML code into small Astro components is good for maintainability, we noticed that each additional component slowed the generation.&lt;/p&gt;

&lt;p&gt;We also realized that building a static site in our case didn’t necessarily require a dedicated HTML framework. Without a framework, we would get full control over orchestration: how data is read, how generation is parallelized, in which order pages are generated and so on. This led us to abandon frameworks altogether and look for a programming language + templating engine combination instead. We evaluated both Java and Go-based solutions by creating test ports that proved their performance benefits. We chose Java because the rest of our system was already built with it. For a templating engine we picked &lt;a href=&quot;https://jte.gg&quot;&gt;JTE&lt;/a&gt; for its promise of speed and type safety.&lt;/p&gt;

&lt;h2 id=&quot;planning-the-porting&quot;&gt;Planning the Porting&lt;/h2&gt;

&lt;p&gt;I might not have dared to start thinking about AI-assisted code porting without previous experience. Luckily, I had already ported my personal chess game project written in Java over 10 years ago into a web-based version. The porting was a success, so I was fairly convinced that AI had reached a level where even larger project’s technology — several tens of thousands of lines — could be reasonably switched. The experience also gave me some intuition about how to approach a port and what to expect.&lt;/p&gt;

&lt;p&gt;To begin code porting with AI, you need two things: An LLM (Large Language Model), which is essentially a machine that is used to generate text based on statistics and probabilities, and an AI agent that is able to figure things out on its own by using the LLM. In my case, the tool of choice was &lt;strong&gt;GitHub Copilot&lt;/strong&gt; (the VS Code extension with agentic capabilities) and &lt;strong&gt;Claude Opus 4.6&lt;/strong&gt; model.&lt;/p&gt;

&lt;p&gt;In theory, you have a chance to get a decent result with a simple prompt like: &lt;em&gt;“Port this project to Java”&lt;/em&gt;. In practice, however, you’ll get much better results by providing good context for the process. For example, it was beneficial for us to write an initial plan on how the Astro-specific page orchestration should look in the Java port and provide the idea as a context for the AI. While AI is also pretty good at solving tooling differences on its own and can ask clarifying questions, it still helps if you make important choices early on. This of course requires sufficient understanding on both the source and target technologies.&lt;/p&gt;

&lt;p&gt;Once the plan is ready, you can give it to the AI as context in Copilot’s planning mode for further analysis. In my case, the AI agent was able to create an execution plan for the order in which things should be ported. For example, domain objects that are used everywhere in the project were clear candidates to port first, since their shape affects the entire codebase.&lt;/p&gt;

&lt;h2 id=&quot;porting-process&quot;&gt;Porting Process&lt;/h2&gt;

&lt;p&gt;One challenge during the porting was that I occasionally noticed things going in the wrong direction mid-process. Fortunately, I was able to guide the agent by providing more details (“steering”) while it was creating the port. This turned out to be highly worthwhile: if the AI “forgot” to follow my instructions early on, it didn’t follow them later either. Fixing issues as early as possible helped steer the whole process in the right direction.&lt;/p&gt;

&lt;p&gt;However, giving steering instructions sometimes caused the agent to stop the porting process entirely after fixing a single issue. I could resume it with a simple &lt;em&gt;“continue”&lt;/em&gt; command, but an even better approach turned out to be prefixing my instructions with &lt;em&gt;“by the way”&lt;/em&gt;, causing the agent to correct its behavior on the fly. This way, I was able to continuously review and guide the AI’s work while it was doing its thing.&lt;/p&gt;

&lt;h2 id=&quot;findings-after-initial-porting&quot;&gt;Findings After Initial Porting&lt;/h2&gt;

&lt;p&gt;In our case, the actual code porting took a couple of hours. This covers the time from the first prompt to the point where the new generator code compiled successfully. A couple more hours were spent to ensure all the ported tests were green before actually trying to run the generator.&lt;/p&gt;

&lt;p&gt;After AI considered the port &lt;em&gt;“done”&lt;/em&gt;, I ran it and immediately experienced a multifold speed improvement compared to our original generator! It was a joy to see the first output: the familiar-looking HTML website we had created but this time generated by an entirely different technology.&lt;/p&gt;

&lt;p&gt;At this point, it was clear that the new system was functioning, so it was time to start analysing the quality of the code more closely. Diving deeper into the generated code, we found several issues:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;File and folder mapping was confusing&lt;/strong&gt;, meaning that there was no clear A-to-B relationship between the source files / folders and the ported system, which made reviewing much harder. In some cases the difference was justified by the architectural differences, but AI had clearly used its own imagination of how the project should be structured even if the original structuring was quite clear. Thus, I ended up starting the whole porting process from scratch and asked the AI to prefer a one-to-one relationship between files and folders when porting code (with some exceptions).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Code duplication.&lt;/strong&gt; The original codebase had many shared utilities used across the generator. In the ported code, the AI didn’t always understand to reuse these utilities and instead created new inline solutions.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Unused code.&lt;/strong&gt; The AI created various helper functions that it ended up not using at all. This might have been partly caused by architectural differences between the source and target systems. It caused unnecessary confusion since after the port was done, we ended up reviewing and reasoning code that turned out to be dead.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Missing or replaced comments.&lt;/strong&gt; Important code comments from the original source were often left out. Even worse, the AI sometimes added its own comments explaining how the ported Java code differs from the original TypeScript. Such comments are pointless since the original Astro project will eventually be removed.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Non-idiomatic code.&lt;/strong&gt; The ported code was technically functional but not always “Java-like”. For example, the AI had ported our manually written number formatting logic directly from TypeScript to Java even if there was built-in support for such formatting directly in Java. AI just didn’t dare to take advantage of it.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Accidental bug discovery.&lt;/strong&gt; Perhaps the most “entertaining” finding was that the porting process revealed bugs in the original implementation! For example, the original generator was creating a few unnecessary pages with empty content. I noticed this when I was arguing with the AI about generation rules that differed from the original. It turned out the AI had independently changed the rules — not something I knew I wanted, but this time it was actually correct!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Despite these problems, the most significant finding was that logical errors were almost nonexistent in the ported generator code: there were only a few and they were easy to fix. It seems that AI is pretty good at porting code between languages and keeping the result functionally equivalent. Also our Astro components, which range from simple to more complex HTML templates, were cleanly converted to JTE code without any major issues.&lt;/p&gt;

&lt;p&gt;I’m not sure whether addressing every possible issue in the initial prompt would have produced a perfect result. After all, it’s easy to tell an AI not to make mistakes, but since porting a system to another technology is a lengthy process, the AI might not “remember” to follow every instruction at every step. Duplication and dead code can also happen by accident when ported code is being refactored. Thus, I believe the initial port — even if working — should only be treated as a starting point towards the final version.&lt;/p&gt;

&lt;h2 id=&quot;ensuring-code-quality&quot;&gt;Ensuring Code Quality&lt;/h2&gt;

&lt;p&gt;Ensuring the quality of such a large-scale ported system is challenging. First and foremost, it’s important to understand that the quality of the ported code is highly dependent on the quality of the source system itself. For example, if the source system has good test coverage, you are already at a good starting point. Also, code structuring, method naming and general &lt;em&gt;feeling&lt;/em&gt; of the quality will be strongly retained in the ported version.&lt;/p&gt;

&lt;p&gt;To begin analysing the quality of the ported code, we used static code analysis tools to verify that the ported generator did not break our coding rules. We also used a diffing tool to ensure the new generator produced identical output to the original generator with the same input data (not counting irrelevant details like formatting differences). Luckily, fixing little things here and there also forced me to check whether I could make sense of the AI-ported code as a human developer.&lt;/p&gt;

&lt;p&gt;We slowly progressed towards the point in which classic rules-based tools stopped finding issues, diffing showed no change in functionality and I could not find problems by reading the new code. Still, I wasn’t confident that there were no more problems to find. So I began thinking…&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/what-i-learned-from-porting-code-with-ai/galaxybrain.avif&quot; alt=&quot;Expanding brain meme&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Yes: it was time to use AI to analyze its own code! While I do not recommend blindly trusting AI in this process, I believe it can reveal interesting findings that other methods might miss.&lt;/p&gt;

&lt;p&gt;While a simple prompt like &lt;em&gt;“Review this ported code”&lt;/em&gt; could produce desired results, I found that giving a specific angle for the analysis provided much better results. Here are some of the prompts that provided good results when run multiple times with different agents. Even better results can be obtained by focusing the analysis on a specific part of the ported codebase.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;“This project was ported from Astro to Java. Can you find duplicates that could use shared helpers?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;“… Can you find unused code or code that is only used in tests?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;“… Can you find functionality that could be implemented more idiomatically in Java?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;“… Can you find security issues in the ported implementation?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;“… Can you find any functional differences between the original and ported implementation that could cause different output?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;“… Can you find places where null handling differs between the original TypeScript and the ported Java, such as unchecked nulls?”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Despite all the automated code reviewing, I feel that human code review still remains highly necessary. After all, the main purpose of programming languages is not to tell a computer what to do, but to tell &lt;em&gt;another human&lt;/em&gt; what a computer should do. Thus, the ported code is not usable if a human cannot understand it.&lt;/p&gt;

&lt;p&gt;As mentioned previously, our plan was to import not only the generator code itself, but also its tests. Some tests were written in Playwright which did not need porting at all since we could just run them normally against the new generated output. Still, there were many unit tests originally written in TypeScript for Vitest and now ported to Java that obviously needed human observation. Green tests mean nothing in the end if they do not test the right things. Luckily, AI had been quite clever also when porting test code — most of the needed fixes were related to using common patterns in the test code.&lt;/p&gt;

&lt;p&gt;We manually reviewed the ported code and its tests until our feeling was strong enough that the result was merge-ready. When an issue was found, we investigated whether a similar issue was also found elsewhere in the ported codebase (with and without the help of AI) and very often found multiple things to fix. This helped create certainty that most of our findings were fixed throughout the whole ported codebase, not just in a single file.&lt;/p&gt;

&lt;h2 id=&quot;verdict&quot;&gt;Verdict&lt;/h2&gt;

&lt;p&gt;The main motivation of switching the generator’s technology was to gain performance benefits that were simply impossible to get with the old implementation. This goal was clearly reached, so the only question remains: did we manage to create a port that’s just as high-quality as if it had been written from scratch?&lt;/p&gt;

&lt;p&gt;As mentioned, there were numerous small problems in the first version of the ported code. The initial port was ready in a single work day, but cleaning, refactoring, testing and reviewing the whole thing took a couple of weeks of work. Despite this, the AI-generated code was still a much better starting point than trying to re-create the whole thing manually from scratch. Of course, it would have been best to implement it this way right from the start, but I’m quite happy with the end result we got by porting the existing code with AI.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;One of the most significant shifts brought by agentic AI is that “wrong” technology choices no longer have to be permanent. What previously meant months of expensive rewriting can now be approached as a structured, AI-assisted process with significantly less time. Choices and their reasoning still matter, but the fear of being locked into a suboptimal stack is considerably smaller than it used to be.&lt;/p&gt;

&lt;p&gt;That said, AI-assisted porting is not a single-step operation. Badly written system won’t automatically become better if ported to another technology. Also, the initial port will almost certainly contain duplication, dead code, missing documentation, and non-idiomatic patterns. Treating the first working version as a foundation to improve upon is the right mindset. Good upfront context, active steering during the process, and thorough quality assurance afterwards — combining static analysis, diffing, targeted AI review, and human code review — are all necessary ingredients for a good end result.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>An experiment in AI-augmented board game design</title>
      <link>http://dev.solita.fi/2026/04/21/ai-assisted-boardgame-development.html</link>
      <pubDate>Tue, 21 Apr 2026 00:00:00 +0000</pubDate>
      <author>open@solita.fi (Solita Oy)</author>
      <guid>http://dev.solita.fi/2026/04/21/ai-assisted-boardgame-development</guid>
	  
      <description>&lt;h2 id=&quot;the-idea&quot;&gt;The idea&lt;/h2&gt;

&lt;p&gt;I like creating games. Board games, card games, dice games — the kind you spread across a table with friends and argue about rules for twenty minutes before anyone actually plays. But I have a problem that every hobby designer knows well: I get stuck.&lt;/p&gt;

&lt;p&gt;Not stuck on the big idea. The big idea is easy. “Pirates fighting for control of a council using dice and cards” — that took about five minutes. The hard part is everything after. Balancing 50 cards across three power levels. Making sure the trick-taking mechanic actually works with the dice drafting and writing rules that a human being can read without falling asleep. These are the parts where projects stall, sit in a folder for six months, and quietly die.&lt;/p&gt;

&lt;p&gt;So, I had a thought: what if I approached this differently? What if instead of doing everything myself and burning out halfway through, I tried to use AI as a genuine collaborator? Not just “ask Claude to write some flavour text” but actually build an agentic workflow — where AI agents have defined roles, shared context, and can generate real, usable output.&lt;/p&gt;

&lt;p&gt;That was the experiment. Build the game &lt;em&gt;and&lt;/em&gt; build the system for building the game, at the same time.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-game-folder&quot;&gt;The Game Folder&lt;/h2&gt;

&lt;p&gt;The first real decision was the format. If agents were going to help design and generate content, they needed structure. Not a Google Doc. Not a wiki. Markdown files in a folder.&lt;/p&gt;

&lt;p&gt;I broke the game down into its moving parts and gave each one a file:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;goal.md&lt;/code&gt; — what are you trying to win?&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setup.md&lt;/code&gt; — what’s on the table when you start?&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turn.md&lt;/code&gt; — what happens each round?&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dice.md&lt;/code&gt; — six faces, four colours, and the actions they trigger&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;card.md&lt;/code&gt; — how cards are structured and costed&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;influence.md&lt;/code&gt; — the scoring track&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pirate.md&lt;/code&gt; — four characters with unique abilities&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;court-market.md&lt;/code&gt; — the economy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each file is self-contained, but they reference each other. A card’s cost is defined in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;card.md&lt;/code&gt;, but it relates to dice faces from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dice.md&lt;/code&gt; and resources from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setup.md&lt;/code&gt;. The whole &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;game/&lt;/code&gt; folder is effectively a relational database written in prose.&lt;/p&gt;

&lt;p&gt;This turned out to be one of the best decisions in the project. When I change how dice work, I update one file. When an agent needs to generate cards, it reads the relevant files and has full context. No ambiguity, no stale information buried in a long conversation thread.&lt;/p&gt;

&lt;p&gt;The next step would be to include a package to generate a knowledge graph to enable more token-efficient handling of the game data in the agents. Given that the game context is large enough to make it worth it.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-agent-phase&quot;&gt;The Agent Phase&lt;/h2&gt;

&lt;p&gt;With the rules in structured markdown, I started building agents. The main agent (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;agent.md&lt;/code&gt;) became a router — a traffic controller that takes a command like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#make-cards-lv2&lt;/code&gt; and knows exactly which subagent to call, what files to feed it, and where to save the output.&lt;/p&gt;

&lt;p&gt;The first subagent was the rules generator. I wrote &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;agent-makerules.md&lt;/code&gt; with detailed instructions: read all the game source files, compile them into a single, coherent rulebook with a table of contents, include directives for appendices, and page break annotations for PDF output.&lt;/p&gt;

&lt;p&gt;The card generator came next. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;agent-makecards.md&lt;/code&gt; was also tricky to get right since I had to be very strict, since the agent kept inventing its own rules and inventing novel ways to allocate build points. But when it started working, it read the card construction rules, read the dice mechanics, read the pirate theme word list, then generated Level 1,2 and 3 cards with specific distributions to make each card set balanced. The agent reads the game files as shared context, so every generated card is mechanically consistent.&lt;/p&gt;

&lt;p&gt;Then I needed to turn markdown into something people could actually use. This is where Claude became indispensable. Together, we started to build skills:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;md-to-html&lt;/strong&gt; was the first. A Node.js script using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;marked&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cheerio&lt;/code&gt; that converts markdown to styled HTML with six different themes. It handles include directives (so the rulebook can pull in the quick reference and strategy guides), heading offsets, page break annotations, and section classification. The pirate theme has skull ornaments and parchment backgrounds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;html-to-pdf&lt;/strong&gt; came next. A Puppeteer-based converter that launches a headless browser, loads the HTML with screen media emulation (so the CSS renders faithfully), and prints to PDF.&lt;/p&gt;

&lt;p&gt;Each skill is self-contained in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.github/skills/&lt;/code&gt; with its own &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SKILL.md&lt;/code&gt; instruction file and scripts. The main agent knows when to call each one. Type &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#make-rules-pdf&lt;/code&gt; and it chains md-to-html, then html-to-pdf. Type &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#make-rules-astro&lt;/code&gt; and it runs md-to-book, then copies the print-friendly files.&lt;/p&gt;

&lt;p&gt;The agent commands grew organically. Every time I thought “I keep doing these three steps manually,” I added a command. The agent file became a menu of one-word triggers that execute multi-step pipelines.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-publishing-phase&quot;&gt;The Publishing Phase&lt;/h2&gt;

&lt;p&gt;At some point, the game had rules, cards, characters, and PDFs. But sharing PDFs felt limiting. I wanted a browsable site — something that looked good, had navigation, and could be updated easily.&lt;/p&gt;

&lt;p&gt;I found an Astro wiki-book template that was close to what I needed. Astro’s static site generation was perfect: markdown in, HTML out, fast, light in footprint. I set up the project in an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;astro-site/&lt;/code&gt; subfolder and started adapting.&lt;/p&gt;

&lt;p&gt;The template used a book/chapter structure with YAML metadata and markdown content — now I needed an md-to-book skill:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;md-to-book&lt;/strong&gt; It takes a markdown file and splits it into a site-compatible book structure — a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;book.yaml&lt;/code&gt; metadata file and individual &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chapter-XX.md&lt;/code&gt; files with proper formatting. It resolves &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;includes&lt;/code&gt;, rewrites image paths and ensures every piece of content sits under a heading that matches its section entry.&lt;/p&gt;

&lt;p&gt;A bit of configuration (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;srcDir&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;publicDir&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;outDir&lt;/code&gt; pointing into the subfolder) and the site was reading generated content.&lt;/p&gt;

&lt;p&gt;Then the theming. The template’s default look was clean but generic. I rewrote &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;global.css&lt;/code&gt; with a pirate colour palette — parchment backgrounds, red accents, gold highlights, Palatino serif fonts. The chapter pages got small-caps headings, dotted borders on subheadings, accent-coloured bold text, and skull-and-crossbones dividers between sections. It looks like something you’d find in a captain’s quarters.&lt;/p&gt;

&lt;p&gt;For print-friendly views, I added a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;linkInsteadOfParts&lt;/code&gt; property to the book system. Books with this property show their linked HTML file in a full-height iframe instead of chapter navigation. The print-friendly rulebook, card sheets, and character sheets are all accessible this way — same content, different presentation.&lt;/p&gt;

&lt;p&gt;The last piece was making republishing painless. After rule changes, regenerating the site rules is a series of agent commands:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;#make-rules           → rebuild the rulebook from game sources
#make-rules-pdf       → generate rules html + pdf
#make-rules-astro     → generate book chapters + copy print files(html files)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The next logical step is to make a publishing sub-agent that handles all of this, so that the top agent only has the different publish commands like #publish-rules.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm run dev&lt;/code&gt; to preview. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm run build&lt;/code&gt; to ship.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;diagram-of-the-solution&quot;&gt;Diagram of the solution&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/img/2026-04-ai-assisted-boardgame-development/diagram.jpg&quot; alt=&quot;diagram.jpg&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;how-it-turned-out&quot;&gt;How It Turned Out&lt;/h2&gt;

&lt;p&gt;I set out to create a board game. What I actually built was two things: a pirate-themed dice-and-card game &lt;em&gt;and&lt;/em&gt; an ai assisted content pipeline.&lt;/p&gt;

&lt;p&gt;The game folder is the source of truth. Edit a rule, run a few commands, and the rulebook, HTML files, PDFs, card sheets, and website all update. The agents generate cards that are mechanically consistent with the rules. The skills convert between formats without manual intervention. The Astro site presents everything in a browsable, themed interface.&lt;/p&gt;

&lt;p&gt;It’s not what I planned. I planned to make a game. The pipeline just… happened, one problem at a time. “I need cards” became an agent. “I need a PDF” became a skill. “I need a website” became a static site generator. Each piece solved an immediate problem, but they composed into something larger.&lt;/p&gt;

&lt;p&gt;Claude has been a tremendous help in this process. Not just as a code generator but as a design partner — suggesting approaches, catching edge cases, writing a lot of code. The agentic workflow turned what would have been a solo grind into something that felt collaborative and, honestly, fun.&lt;/p&gt;

&lt;p&gt;Funny how it turns out sometimes. You start with a pirate game and end up building a content generation and deployment pipeline.&lt;/p&gt;

&lt;p&gt;Ohh, by the way, I set up my GitHub to talk to Vercel, so pushing to main builds the astro site out to: https://pirates-court.vercel.app&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>What happens to Code Quality and Developer Role in the AI era</title>
      <link>http://dev.solita.fi/2026/02/20/code-quality-in-the-ai-era.html</link>
      <pubDate>Fri, 20 Feb 2026 00:00:00 +0000</pubDate>
      <author>open@solita.fi (Solita Oy)</author>
      <guid>http://dev.solita.fi/2026/02/20/code-quality-in-the-ai-era</guid>
	  
      <description>&lt;p&gt;I have been working at Solita for soon 25 years. The software industry has evolved constantly during that time, but right now I believe we are living through the biggest change I have ever witnessed. I &lt;a href=&quot;/2015/10/19/awesomeness-of-spring-boot-13-fully-executable-jars.html&quot;&gt;seldom&lt;/a&gt; write blog posts, but I want to publish these bold — and maybe ridiculous — statements on the Internet so I can look back and reflect on them after the next 25 years. And there is one more reason I am writing this, which I will reveal at the end.&lt;/p&gt;

&lt;h2 id=&quot;background-the-coding-janitor&quot;&gt;Background: The Coding Janitor&lt;/h2&gt;

&lt;p&gt;I have been a Software Architect for a long time. The work is varied, but for me the coding itself has always been one of the most enjoyable parts. Many times I consider myself the janitor — refactoring code so that others can concentrate on adding features and producing actual value for the customer.&lt;/p&gt;

&lt;p&gt;I have tried to keep standards and quality high so I can be proud of the output and so that new team members can join easily and maintain the codebase. One important aspect of maintaining good quality is code reviews. Finding bugs is not the main point. Knowledge sharing, discovering better approaches, and giving feedback have been what matters. The aim is to build a better product and a better team.&lt;/p&gt;

&lt;p&gt;I am quite new to this AI world. Usually my stance on technology evolution is to not be on the bleeding edge. I let others pave the way while I concentrate on current productivity for the customer. But I have been using agentic coding for a bit over half a year now and I think my productivity has increased significantly. Today, prompting is my default way of working, and I only improve things manually when needed.&lt;/p&gt;

&lt;h2 id=&quot;the-fundamental-change&quot;&gt;The Fundamental Change&lt;/h2&gt;

&lt;p&gt;In the future, I won’t be writing code as much anymore because LLMs can already do a lot — and they will do even more tomorrow.&lt;/p&gt;

&lt;p&gt;I have always believed that good code is easily readable. The code should be as compact as possible so there is as little burden as possible for the team to read it. But LLMs generate quite verbose code. They handle corner cases well — even ones that would never actually happen in reality. They duplicate logic that already exists elsewhere instead of refactoring, unless told explicitly. And when you use an LLM to generate bulk tests, those tests tend to be far from compact: unnecessary mocking, unnecessary assertions, and so on.&lt;/p&gt;

&lt;p&gt;Verbose code means more code for your colleagues to read. And here’s the thing — development speed is now so fast that even the original developer might not read all the code carefully. Of course, you can instruct the LLM to behave more as you like.&lt;/p&gt;

&lt;p&gt;But this raises an uncomfortable question: &lt;strong&gt;what is the purpose of striving for readable code in the LLM era?&lt;/strong&gt; An LLM does not blame anyone, does not get frustrated, and does not suffer lower productivity when working in a bad codebase — unlike humans. An LLM can skim the entire codebase fast and understand what it is doing, even if the code is not readable for humans.&lt;/p&gt;

&lt;p&gt;It is said that someone needs to be the reviewer for the LLM. Oh boy, that might be a boring task — because the human aspect diminishes. Helping others, and learning from them, is not as rewarding anymore. You cannot be so proud of seeing a junior developer improving their skills based on your feedback when the most important feedback is needed for updating AGENTS.md.&lt;/p&gt;

&lt;p&gt;And what if everybody gets frustrated reading verbose code and just starts accepting all code changes? What if there is no one to say &lt;em&gt;“Stop. We are building on the wrong assumption!”&lt;/em&gt; — while the LLM continues to build on that assumption happily? So you still need to review things carefully in the future. But reviewing becomes a less rewarding job.&lt;/p&gt;

&lt;h2 id=&quot;productivity-improves&quot;&gt;Productivity Improves&lt;/h2&gt;

&lt;p&gt;Seniority in software development is not needed as much in the future. Development becomes more achievable for everyone. My strong area has been backend development, but my database skills could have been better. Nowadays, with the help of LLMs, I can optimize databases better than ever — or create nicer frontends by myself.&lt;/p&gt;

&lt;p&gt;I work a lot for the public sector. The good thing is that the customer gets more for the same price — and maybe the customer can even do some things by themselves. That productivity increase is something I appreciate as a taxpayer.&lt;/p&gt;

&lt;p&gt;A good software professional has always needed strong communication skills, and those are still needed in the future. But maybe it is just not as rewarding to communicate with agents…&lt;/p&gt;

&lt;p&gt;The Software Architect’s job is not ending. I have just been in a very hands-on role, writing a lot of code. In the future I might not have that luxury. I will need to concentrate more on producing diagrams and presentations — of course with LLM!&lt;/p&gt;

&lt;p&gt;I do not want to sound like a doomer. Change is inevitable and I need to adapt. There is no purpose in missing the old times. I need to find enjoyment in something other than writing code. Will I? I don’t know yet. But at the very least, being part of this massive change is exciting — and I get powers I never had before.&lt;/p&gt;

&lt;h2 id=&quot;post-scriptum&quot;&gt;Post Scriptum&lt;/h2&gt;

&lt;p&gt;Remember the one reason I mentioned at the beginning? Here it is: &lt;strong&gt;this might be one of the last times to generate something truly by yourself.&lt;/strong&gt; In the future, AI writes these blog posts — and nobody reads them because they are written by AI!&lt;/p&gt;

&lt;p&gt;This post is written by AI too. Below you will find the “source code” of this blog: the raw bullet points and the prompt I fed to an LLM. Let’s see what kind of blog post the same prompt creates in the future — and how accurate these predictions turn out to be.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;ai-summary-how-have-these-predictions-held-up-february-2026&quot;&gt;AI Summary: How Have These Predictions Held Up? (February 2026)&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;This section was written by an AI at the time of publication.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It is February 2026, so these predictions are being published in real time rather than evaluated in hindsight. However, we can note how they align with the current trajectory:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;LLMs generating verbose code&lt;/strong&gt; — This is a widely recognized issue. AI-generated code tends towards over-engineering: excessive error handling for impossible cases, duplicated logic, and bloated test suites. Tools are improving, but the verbosity problem remains real. The observation is spot on.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Code quality mattering less&lt;/strong&gt; — This is the most provocative claim. Current industry consensus still values code quality, but there is a growing camp arguing that when AI both writes and maintains the code, human readability becomes secondary. Too early to call definitively, but the direction is worth watching.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Review becoming less rewarding&lt;/strong&gt; — Early signs are visible. Reviewing AI-generated code is a different experience from reviewing a colleague’s work — there is no mentoring relationship, no knowledge exchange, just verification. Some teams report review fatigue. The concern is valid.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Reduced need for deep specialization&lt;/strong&gt; — Partially validated. LLMs do flatten the expertise curve for many tasks. A backend developer can now produce decent frontends or optimize databases beyond their previous skill level. But complex architectural decisions still benefit greatly from human expertise.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;AI writing blog posts that nobody reads&lt;/strong&gt; — A delightfully self-referential prediction. The bullet points are human. The prose is AI. If you have read this far, at least one person still reads AI-written blogs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Check back in 2051 to see how these bold statements aged.&lt;/em&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-source-code&quot;&gt;The Source Code&lt;/h2&gt;

&lt;p&gt;Prompt: &lt;em&gt;Create blog post based on these bullet points and add brief AI summary at the end how these predictions have stood times. Leave this part at the end as unedited “raw source code”.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Background: The Coding Janitor&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;I have been working at Solita soon for 25 years. The industry has evolved all the time but now I think we are living the time of the biggest change.&lt;/li&gt;
  &lt;li&gt;I write blogs &lt;a href=&quot;/2015/10/19/awesomeness-of-spring-boot-13-fully-executable-jars.html&quot;&gt;seldom&lt;/a&gt; but I want to publish these bold and maybe ridiculous statements to Internet so I can reflect this after 25 next years&lt;/li&gt;
  &lt;li&gt;And one reason why I am doing this I tell later.&lt;/li&gt;
  &lt;li&gt;I have been Software Architect for long time. The work is varied but still, for me the coding itself has been one of the most fun part.&lt;/li&gt;
  &lt;li&gt;Many times I consider myself as the janitor refactoring code so other’s can concentrate on adding features and producing actual value for the customer.&lt;/li&gt;
  &lt;li&gt;I have tried to keep standards and quality high, so I can be proud of the output and enable new team members to join easily to maintain the codebase.&lt;/li&gt;
  &lt;li&gt;One aspect of maintaining good quality are code reviews. Finding bugs is not the main point. Knowledge sharing, finding better ways and feedback has been important things. Aim is to build better better product and better team.&lt;/li&gt;
  &lt;li&gt;I am quite new for this AI world. Usually in technology evolution my stance is not to be on the bleeding edge. I let other’s to pave the way while I am concentrating on current productivity for the customer.&lt;/li&gt;
  &lt;li&gt;I have used agentic coding bit over half a year and I think my productivity has increased much. Nowadays the prompting is my default way and then improve things manually if needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fundamental change&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;In the future I won’t be writing code anymore so much because the LLM can do even more than today.&lt;/li&gt;
  &lt;li&gt;I think good code is easily readable. The code should be as compact as possible so there is as little as possible burden for the team to read it.&lt;/li&gt;
  &lt;li&gt;I think nowadays LLMs generate quite verbose code. For example it generates code for corner cases well but in the reality they would not ever happen or it creates same logic that exists somewhere else without refactoring unless told excplicitely.&lt;/li&gt;
  &lt;li&gt;Also normal way is to use LLM to generate bulk tests for the code. I think those test are as compact as possible. They contain unnecessary mocking, unncesessary assertions etc.&lt;/li&gt;
  &lt;li&gt;Verbose code leads to more code for colleague to read.&lt;/li&gt;
  &lt;li&gt;Heck, the development speed is so fast that even the original developer might not read the all the code carefully.&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Of course LLM can be instructed to behave more as you like.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;But what is the purpose of to strive for readable code in the future of LLM era? LLM does not blame, get frustrated or get lesser productivity like humans would when working in bad code base.&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;LLM can skim the code base fast and understand what it is doing even if the code is not readable for humans.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;It is said that someone needs to be the reviewer for the LLM.&lt;/li&gt;
  &lt;li&gt;Oh boy, that might be boring task because the human aspect diminishes. Helping (and learning from!) others is not so much rewarding anymore.&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;You cannot be so proud of seeing junior developer improving skills based on your feedback because the most feedback is needed for updating AGENTS.md.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;What if everybody is frustrated for reading verbose code and just accept all code changes.&lt;/li&gt;
  &lt;li&gt;What if there is no one to say “Stop. We are building on wrong assumption!”. The LLM continues to build on that happily…&lt;/li&gt;
  &lt;li&gt;So you need to review things carefully also in the future. But the reviewing is less rewarding job.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Productivity improves&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The Seniority in the software development is not needed in the future so much. Development gets more achievable for everyone.&lt;/li&gt;
  &lt;li&gt;My strong area has been in the backend development but my database skills could have been better.&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Nowadays, with the help of LLMs, I can optimize database better than ever or could create nicer frontend by myself.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;I am working a lot for the public sector.&lt;/li&gt;
  &lt;li&gt;The good thing is that the customer gets more with the same price and maybe it can do something by itself.&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;That productivity increase is good as a tax payer.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;Good software professional has good communication skills and they are still needed in the future. Maybe it is just not so rewarding to communicate with agents…&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The Software Architect’s job is not ending. I just have been in very hands on role, coding much. In the future I might not have that luxury. I need to concentrate on producing diagrams and presentations more, of course with LLM!&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;I don’t want to sound a doomer. Change is inevitable and I need to adapt. There is not purpose to miss old times.&lt;/li&gt;
  &lt;li&gt;I need to find fun from something else than writing code. Shall I find? At least the big change is fun to be part of and get powers I never had.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Post script&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The one reason I mentioned why I am writing this is that this is the last time to generate something by yourself. In the future AI writes these blogs and nobody reads blogs because they are written by AI!&lt;/li&gt;
  &lt;li&gt;Like this post is written by AI - There is the “source code” and prompt of this blog. Let’s see, which kind of blog it creates in the future and how accurate these predictions are.&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>MCP: Connecting Language Models to the Real World</title>
      <link>http://dev.solita.fi/2026/02/16/mcp-what-and-how.html</link>
      <pubDate>Mon, 16 Feb 2026 00:00:00 +0000</pubDate>
      <author>open@solita.fi (Solita Oy)</author>
      <guid>http://dev.solita.fi/2026/02/16/mcp-what-and-how</guid>
	  
      <description>&lt;p&gt;I’ve been working with FunctionAI MCP Servers since last autumn, and it’s been fun to work with such an interesting technology that could be one of the first versions of a standardized framework for making queries to data sources using a large language model (LLM). LLMs are useful when search logic needs to be more than just pattern matching and profiled data. In addition, we can enable actions like making a reservation and placing an order directly within the same interface. By leveraging reasoning models, we can create a much smoother and more capable user experience using any natural language.&lt;/p&gt;

&lt;p&gt;I’m not with all the AI hype going around; I’m usually the skeptic in the room. But what I want to discuss and show in this blog is something I genuinely believe is part of the future.&lt;/p&gt;

&lt;h2 id=&quot;introduction-what-is-mcp&quot;&gt;Introduction: What is MCP?&lt;/h2&gt;

&lt;p&gt;If you’ve been working with LLM’s lately, you’ve probably noticed a recurring challenge: how do you give these powerful AI systems access to your data, tools, and services in a reliable and standardized way? Every integration seems to require custom code, special handling, and maintenance overhead. Prompting with ChatGPT might require hand-picking documents that you add as attachment for enhanced context every time you open a new conversation.&lt;/p&gt;

&lt;p&gt;Enter the &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt; an open protocol that standardizes how applications provide context to LLMs. Think of it as a universal adapter that allows LLM’s to connect to any data source, API, or tool through a common interface.&lt;/p&gt;

&lt;p&gt;MCP was developed by Anthropic and released as an open standard, designed to solve the fragmentation problem in AI integrations. Instead of building custom integrations for every data source you want to connect to your LLM, MCP provides a unified way for applications to expose their capabilities to AI systems.&lt;/p&gt;

&lt;h3 id=&quot;architecture-overview&quot;&gt;Architecture Overview&lt;/h3&gt;

&lt;p&gt;MCP follows a client-server architecture:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;MCP Host&lt;/strong&gt;: AI application that provides an LLM and manages MCP Clients.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;MCP Client&lt;/strong&gt;: The Client is connected to an MCP Server and provides the context to the Host&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;MCP Server&lt;/strong&gt;: A standardized interface that exposes primitives to MCP Client which are capabilities that the Server provides:
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Resources&lt;/strong&gt;: Data or content that can be read (files, database records, API responses)&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Tools&lt;/strong&gt;: Functions that can be invoked to perform actions (code)&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Prompts&lt;/strong&gt;: Pre-configured prompt templates that can be used to guide interactions&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To make a distinction between a MCP Server and MCP Client, you can think of MCP Servers as REST API’s but for the MCP Clients. The Host has the LLM and &lt;em&gt;n&lt;/em&gt; amount of MCP Clients, but the Client handles the connection to the MCP Server, you can think of MCP Client as the front-end. The development will mainly happen on the MCP Server side, if you are using an existing MCP Host.&lt;/p&gt;

&lt;p&gt;The magic happens when you connect these pieces. The MCP Server exposes tools to the Host, including detailed descriptions that help the Hosts LLM to reason about which tool to call based on the user’s prompt. 
Instead of writing custom integration code for each data source, you write one MCP server, and any MCP-compatible client can use it. This dramatically reduces complexity and increases reusability.&lt;/p&gt;

&lt;p&gt;MCP Client also has 3 primitives:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Sampling&lt;/strong&gt;: Allowing MCP Server to request Language Model completion from the Clients LLM and not include SDK on their own.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Elicitation&lt;/strong&gt;: Allow MCP Server to request for additional information from the user, for example, confirmation for an action.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Logging&lt;/strong&gt;: Enable MCP Server to send longs for debugging and monitoring purposes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I can’t cover every detail about the MCP architecture, but you can read more about it &lt;a href=&quot;https://modelcontextprotocol.io/docs/learn/architecture&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;real-world-applications-what-can-you-build&quot;&gt;Real-World Applications: What Can You Build?&lt;/h2&gt;

&lt;p&gt;The possibilities with MCP are extensive. Here are some compelling use cases:&lt;/p&gt;

&lt;h3 id=&quot;knowledge-base-integration&quot;&gt;Knowledge Base Integration&lt;/h3&gt;
&lt;p&gt;Connect your LLM to internal documentation, wikis, or knowledge bases. Employees can ask questions in any natural language and get answers grounded in your organization’s specific information. Solita’s FunctionAI for example.&lt;/p&gt;

&lt;h3 id=&quot;flight-information-system-our-example&quot;&gt;Flight Information System &lt;em&gt;(Our Example)&lt;/em&gt;&lt;/h3&gt;
&lt;p&gt;In this blog post, we’ll explore a practical example: a flight information system. We’ll have a look at an MCP server that provides access to flight data. This demonstrates how MCP can transform static data into an interactive, queryable service that responds to natural language.&lt;/p&gt;

&lt;h3 id=&quot;multi-source-intelligence&quot;&gt;Multi-Source Intelligence&lt;/h3&gt;
&lt;p&gt;The real power emerges when you connect multiple plugin-like MCP servers simultaneously. Imagine an assistant that can query your calendar, read/create tickets from your favorite project management tool, and update documentation, all in a single conversation, providing synthesized insights across systems.&lt;/p&gt;

&lt;h2 id=&quot;implementation-flights-mcp-server&quot;&gt;Implementation: Flights MCP Server&lt;/h2&gt;

&lt;p&gt;Let’s walk through a practical MCP server in Python. Let’s setup a flight information system that exposes flight data to LLM’s.&lt;/p&gt;

&lt;h3 id=&quot;setting-up-the-environment&quot;&gt;Setting Up the Environment&lt;/h3&gt;

&lt;p&gt;Before we dive into the example, you need to have Docker installed and some kind of LLM implementation that supports MCP Server connections. I’m using &lt;a href=&quot;https://lmstudio.ai/&quot;&gt;LM Studio&lt;/a&gt; but you can choose to use whatever tool you wish, for example &lt;a href=&quot;https://claude.com/product/claude-code&quot;&gt;Claude Code&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;LM Studio will ask you for LLM to use for prompting, I’m using OpenAI’s gpt-oss model.&lt;/p&gt;

&lt;p&gt;Clone &lt;a href=&quot;https://github.com/JanneTuhkanen/FlightsMCP&quot;&gt;FlightsMCP&lt;/a&gt; repository to your environment. There is a Dockerfile included and a shell script to setup the MCP Server for you.&lt;/p&gt;

&lt;p&gt;Now, execute run_dockerized.sh and wait until the Docker has finished.&lt;/p&gt;

&lt;p&gt;Once Docker is finished setting up. We can connect to our mcp server. For LM Studio, you can setup the connection from top right corner &lt;br /&gt;
Program &amp;gt; Install (on Integrations panel) &amp;gt; Edit mcp.json.&lt;/p&gt;

&lt;p&gt;Add the flights mcp server to the settings like this.&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;mcpServers&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;flights-mcp-server&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;url&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;http://127.0.0.1:8000/mcp&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now you should have connection open to the FlightsMCP server.&lt;/p&gt;

&lt;h2 id=&quot;lets-get-prompting&quot;&gt;Let’s get prompting&lt;/h2&gt;

&lt;p&gt;Now, let’s ask the client for flights. I’m prompting for flights to Oslo.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/mcp-what-and-how/prompt.png&quot; alt=&quot;Prompting&quot; /&gt;&lt;/p&gt;

&lt;p&gt;As you can see, it thinks it should call /flights endpoint since it is an available tool for us to use. Click “Proceed” to give the client permission to call this tool.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/mcp-what-and-how/prompt-result.png&quot; alt=&quot;Flights being listed&quot; /&gt;&lt;/p&gt;

&lt;p&gt;What happens, it gets the full JSON as a response and reasons with our prompt that it needs to filter flights so that flights to Oslo remains.&lt;/p&gt;

&lt;h2 id=&quot;under-the-hood-how-mcp-works&quot;&gt;Under the Hood: How MCP Works&lt;/h2&gt;

&lt;p&gt;Now that we understand what MCP can do, let’s explore how it actually works at a technical level.&lt;/p&gt;

&lt;h3 id=&quot;transport-layer&quot;&gt;Transport Layer&lt;/h3&gt;

&lt;p&gt;Transports in MCP is responsible of communication related mechanics between a client and a server.
MCP supports multiple transport mechanisms:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;stdio&lt;/strong&gt;: Communication through standard input/output (great for local processes)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Streamable HTTP&lt;/strong&gt;: Recommended HTTP transport that supports bidirectional, streaming communication using HTTP POST/GET, optionally with Server‑Sent Events (SSE).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;HTTP+SSE (deprecated)&lt;/strong&gt;: Older HTTP transport from an earlier protocol version, kept only for backwards compatibility with legacy clients/servers&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;json-rpc-the-communication-protocol&quot;&gt;JSON-RPC: The Communication Protocol&lt;/h3&gt;

&lt;p&gt;MCP uses &lt;strong&gt;JSON-RPC 2.0&lt;/strong&gt; as its underlying message format. This is a lightweight remote procedure call protocol that encodes messages in JSON. It’s simple, language-agnostic, and widely supported.&lt;/p&gt;

&lt;p&gt;Here’s what a typical MCP interaction looks like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Client discovers server capabilities:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is what Requests look like. Requests are sent from the client to the server or vice versa, to initiate an operation.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{
  &quot;jsonrpc&quot;: &quot;2.0&quot;,
  &quot;id&quot;: string | number,
  &quot;method&quot;: string,
  &quot;params&quot;: {
    [key: string]: any;
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;2. Server responds with its capabilities:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Responses are sent in reply to requests, containing the result or error of the operation.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{
  &quot;jsonrpc&quot;: &quot;2.0&quot;,
  &quot;id&quot;: 1,
  &quot;result&quot;: {
    [key: string]: any;
  },
  error?: {
    code: number;
    message: string;
    data?: unknown;
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing Thoughts&lt;/h2&gt;

&lt;p&gt;The Model Context Protocol represents a significant step forward in making AI applications more practical and maintainable. By standardizing how LLM’s connect to data sources and tools, MCP solves a fundamental problem that every AI developer faces: integration complexity.&lt;/p&gt;

&lt;p&gt;What makes MCP particularly exciting is its potential for composability. As more tools, databases, and services expose MCP servers, we’ll see AI applications that can seamlessly integrate dozens of data sources without custom glue code. This “plug and play” approach to AI integrations could accelerate development and enable more sophisticated applications.&lt;/p&gt;

&lt;p&gt;However, MCP is still young. The ecosystem is growing, but many tools and services don’t yet have MCP servers. There are also ongoing discussions about best practices, security patterns, and protocol extensions. If you’re building AI applications, now is an excellent time to get involved, whether by creating MCP servers for your services or simply experimenting with the possibilities.&lt;/p&gt;

&lt;h3 id=&quot;further-exploration&quot;&gt;Further exploration&lt;/h3&gt;

&lt;p&gt;If you’re interested in exploring MCP further:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Check out the &lt;a href=&quot;https://modelcontextprotocol.io/&quot;&gt;official MCP specification&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Explore existing MCP servers in the community&lt;/li&gt;
  &lt;li&gt;Build your own MCP server for a data source you work with&lt;/li&gt;
  &lt;li&gt;Join the conversation about where the protocol should go next&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>SQL Isolation Levels</title>
      <link>http://dev.solita.fi/2026/02/13/postgresql-isolation-levels.html</link>
      <pubDate>Fri, 13 Feb 2026 00:00:00 +0000</pubDate>
      <author>open@solita.fi (Solita Oy)</author>
      <guid>http://dev.solita.fi/2026/02/13/postgresql-isolation-levels</guid>
	  
      <description>&lt;p&gt;The concept of ACID regarding transactions is familiar to many programmers. It stands for four properties associated with database transactions.&lt;/p&gt;

&lt;p&gt;The first of these is Atomicity, which means that a transaction is always executed either completely or not at all.&lt;/p&gt;

&lt;p&gt;Consistency means that a transaction cannot violate database integrity rules. A transaction takes the DB from one valid state to another. One of those are foreign key and primary key constraints. This prevents, for example, the existence of a row with a foreign key that references a non-existent primary key.&lt;/p&gt;

&lt;p&gt;Durability ensures that the changes made by a committed transaction are permanent. This is usually implemented with a separate transaction log, which guarantees that the data persists even if the database system crashes after the transaction has been committed.&lt;/p&gt;

&lt;p&gt;I saved Isolation for last because, based on my experience interviewing software developers, this is an area relatively unfamiliar to many.&lt;/p&gt;

&lt;p&gt;Since the best cure for a lack of knowledge is information, this blog post explains what isolation levels are, specifically from the perspective of a PostgreSQL database. I have also created interactive simulators to go through different scenarios with different isolation levels.&lt;/p&gt;

&lt;h1 id=&quot;isolation-in-brief&quot;&gt;Isolation in Brief&lt;/h1&gt;

&lt;p&gt;Isolation defines how a transaction perceives other concurrent transactions during its execution. It represents a trade-off between database performance and the prevention of concurrency anomalies. A higher isolation level typically means that transactions may block each other’s progress or even lead to transaction failures to ensure data integrity.&lt;/p&gt;

&lt;p&gt;To understand these levels, we must first take a brief detour into how PostgreSQL stores data using the MVCC (Multi-Version Concurrency Control) model.&lt;/p&gt;

&lt;h1 id=&quot;mvcc&quot;&gt;MVCC&lt;/h1&gt;

&lt;p&gt;In a PostgreSQL database, records—or more simply, rows—are versioned rather than overwritten. When data is modified, the database creates a new version of the row instead of replacing the old one.&lt;/p&gt;

&lt;p&gt;This is managed using internal metadata fields, specifically Transaction IDs:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;$xmin$&lt;/em&gt; Stores the ID of the transaction that inserted the row version&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;$xmax$&lt;/em&gt; Stores the ID of the transaction that deleted or updated it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From a query perspective, this mechanism ensures that a row cannot “disappear” in the middle of a read operation; the database simply checks these IDs to determine which version of the row is visible to your specific transaction. Consequently, data being added by a parallel process won’t disrupt an ongoing transaction. A new version remains invisible until the transaction that created it has officially committed.&lt;/p&gt;

&lt;p&gt;Understanding this versioning is essential as we move forward to examine the different isolation levels, which we will review from weakest to strongest.&lt;/p&gt;

&lt;h1 id=&quot;isolation-level-1-read-uncommitted-dirty-read&quot;&gt;Isolation level 1: Read Uncommitted (Dirty Read)&lt;/h1&gt;

&lt;p&gt;This one is simple: no sane database — PostgreSQL included — allows a transaction to read uncommitted data. PostgreSQL accepts READ UNCOMMITTED, but it behaves the same as READ COMMITTED.&lt;/p&gt;

&lt;h1 id=&quot;isolation-level-2-read-committed&quot;&gt;Isolation level 2: Read Committed&lt;/h1&gt;

&lt;p&gt;This is the most important level to understand, as it is the default setting in PostgreSQL.&lt;/p&gt;

&lt;p&gt;At this level, each query within a transaction sees only the data committed at the moment the query begins. While this prevents “dirty reads,” it allows for two specific anomalies:&lt;/p&gt;

&lt;p&gt;Non-repeatable Read: If you read the same row twice within the same transaction, you might get different results if another transaction committed changes to that row in between your two queries.&lt;/p&gt;

&lt;p&gt;Phantom Read: This occurs when a query (such as an aggregate or a range scan) returns a different set of rows on a second execution because another transaction committed an INSERT or DELETE in the meantime. A “phantom” row has appeared or disappeared.&lt;/p&gt;

&lt;p&gt;Whether these anomalies are a problem depends entirely on your application logic. A typical risk is making a functional decision or a calculation based on data that changes before the transaction finishes.&lt;/p&gt;

&lt;p&gt;While there are ways to solve these issues at this isolation level—such as using specific locking clauses—we will return to those at the very end of this post.&lt;/p&gt;

&lt;p&gt;To better understand how this works in practice, you can use the following simulator to test how transactions behave when running side-by-side. Keep in mind how Postgresql uses Transaction IDs to check rows visibility in a transaction.&lt;/p&gt;

&lt;style&gt;
    .rc-demo {
        --tx-a: #3b82f6;
        --tx-a-bg: #eff6ff;
        --tx-a-border: #bfdbfe;
        --tx-b: #f59e0b;
        --tx-b-bg: #fffbeb;
        --tx-b-border: #fde68a;
        --tx-old: #6b7280;
        --result-ok: #16a34a;
        --result-ok-bg: #f0fdf4;
        --result-warn: #d97706;
        --result-warn-bg: #fffbeb;
        --committed: #16a34a;
        --uncommitted: #9ca3af;
        --dead-bg: #f9fafb;
        --rc-gray-50: #f9fafb;
        --rc-gray-100: #f3f4f6;
        --rc-gray-200: #e5e7eb;
        --rc-gray-300: #d1d5db;
        --rc-gray-400: #9ca3af;
        --rc-gray-500: #6b7280;
        --rc-gray-600: #4b5563;
        --rc-gray-700: #374151;
        --rc-gray-800: #1f2937;
        --rc-gray-900: #111827;
        --rc-radius: 8px;
        --rc-font-mono: 'SF Mono', 'Fira Code', 'Fira Mono', Menlo, Consolas, monospace;
        --rc-font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    }

    .rc-demo {
        font-family: var(--rc-font-sans);
        color: var(--rc-gray-800);
        line-height: 1.6;
        max-width: 900px;
        margin: 2rem auto;
        padding: 1.5rem;
        border: 1px solid var(--rc-gray-200);
        border-radius: var(--rc-radius);
        background: #fff;
    }

    .rc-demo .rc-title {
        font-size: 1.3rem;
        font-weight: 700;
        margin-bottom: 0.25rem;
        color: var(--rc-gray-900);
    }

    .rc-demo .subtitle {
        color: var(--rc-gray-500);
        font-size: 0.95rem;
        margin-bottom: 1.5rem;
    }

    .rc-demo .tabs {
        display: flex;
        gap: 0.25rem;
        border-bottom: 2px solid var(--rc-gray-200);
        margin-bottom: 1.5rem;
        overflow-x: auto;
    }

    .rc-demo .tab {
        padding: 0.6rem 1rem;
        border: none;
        background: none;
        font-family: var(--rc-font-sans);
        font-size: 0.875rem;
        font-weight: 500;
        color: var(--rc-gray-500);
        cursor: pointer;
        border-bottom: 2px solid transparent;
        margin-bottom: -2px;
        white-space: nowrap;
        transition: color 0.15s, border-color 0.15s;
    }

    .rc-demo .tab:hover { color: var(--rc-gray-700); }
    .rc-demo .tab.active { color: var(--tx-a); border-bottom-color: var(--tx-a); }

    .rc-demo .timelines {
        display: flex;
        gap: 1.5rem;
        margin-bottom: 1.5rem;
    }

    .rc-demo .timeline { flex: 1; min-width: 0; }

    .rc-demo .timeline-header {
        font-size: 0.8rem;
        font-weight: 600;
        text-transform: uppercase;
        letter-spacing: 0.05em;
        padding: 0.4rem 0.75rem;
        border-radius: var(--rc-radius) var(--rc-radius) 0 0;
        margin-bottom: 0;
    }

    .rc-demo .timeline-header .txid {
        font-weight: 400; opacity: 0.7; font-size: 0.75rem;
        text-transform: none; letter-spacing: 0;
    }

    .rc-demo .timeline-header.tx-a {
        color: var(--tx-a); background: var(--tx-a-bg);
        border: 1px solid var(--tx-a-border); border-bottom: none;
    }

    .rc-demo .timeline-header.tx-b {
        color: var(--tx-b); background: var(--tx-b-bg);
        border: 1px solid var(--tx-b-border); border-bottom: none;
    }

    .rc-demo .timeline-body {
        border-radius: 0 0 var(--rc-radius) var(--rc-radius);
        min-height: 120px;
    }

    .rc-demo .timeline-body.tx-a { border: 1px solid var(--tx-a-border); border-top: none; }
    .rc-demo .timeline-body.tx-b { border: 1px solid var(--tx-b-border); border-top: none; }

    .rc-demo .step-card {
        padding: 0.5rem 0.75rem;
        border-bottom: 1px solid var(--rc-gray-100);
        transition: opacity 0.2s, background 0.2s;
    }

    .rc-demo .step-card:last-child { border-bottom: none; }
    .rc-demo .step-card.future { opacity: 0; pointer-events: none; height: 0; padding: 0; overflow: hidden; }
    .rc-demo .step-card.past { opacity: 0.45; }

    .rc-demo .step-card.current-a {
        background: var(--tx-a-bg); border-left: 3px solid var(--tx-a); opacity: 1;
    }

    .rc-demo .step-card.current-b {
        background: var(--tx-b-bg); border-left: 3px solid var(--tx-b); opacity: 1;
    }

    .rc-demo .step-card.idle {
        color: var(--rc-gray-400); font-style: italic; font-size: 0.8rem; padding: 0.35rem 0.75rem;
    }

    .rc-demo .sql {
        font-family: var(--rc-font-mono); font-size: 0.8rem;
        line-height: 1.5; word-break: break-word; white-space: pre-wrap;
    }

    .rc-demo .result {
        display: inline-block; font-family: var(--rc-font-mono); font-size: 0.75rem;
        padding: 0.15rem 0.5rem; border-radius: 4px; margin-top: 0.25rem; font-weight: 600;
    }

    .rc-demo .result-ok { background: var(--result-ok-bg); color: var(--result-ok); border: 1px solid #bbf7d0; }
    .rc-demo .result-warn { background: var(--result-warn-bg); color: var(--result-warn); border: 1px solid var(--tx-b-border); }

    .rc-demo .db-section { margin-bottom: 1.25rem; }

    .rc-demo .db-label {
        font-size: 0.75rem; font-weight: 600; text-transform: uppercase;
        letter-spacing: 0.05em; color: var(--rc-gray-500); margin-bottom: 0.4rem;
    }

    .rc-demo .db-label span { font-weight: 400; text-transform: none; letter-spacing: 0; }
    .rc-demo .db-table-wrap { overflow-x: auto; }

    .rc-demo .db-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }

    .rc-demo .db-table th {
        text-align: left; padding: 0.4rem 0.6rem; background: var(--rc-gray-50);
        border: 1px solid var(--rc-gray-200); font-weight: 600; font-size: 0.75rem; color: var(--rc-gray-600);
    }

    .rc-demo .db-table th.mvcc-col { background: #f0f4ff; color: var(--rc-gray-500); font-size: 0.7rem; }

    .rc-demo .db-table td {
        padding: 0.4rem 0.6rem; border: 1px solid var(--rc-gray-200);
        font-family: var(--rc-font-mono); font-size: 0.8rem; transition: background 0.3s, opacity 0.3s;
    }

    .rc-demo .db-table td.mvcc-cell { font-size: 0.75rem; background: #fafbff; }
    .rc-demo .db-table tr.version-dead td { opacity: 0.4; text-decoration: line-through; }
    .rc-demo .db-table tr.version-dead td.mvcc-cell { text-decoration: none; }
    .rc-demo .db-table tr.version-seen-a { outline: 2px solid var(--tx-a); outline-offset: -1px; }
    .rc-demo .db-table tr.version-seen-b { outline: 2px solid var(--tx-b); outline-offset: -1px; }
    .rc-demo .db-table tr.version-uncommitted td { border-style: dashed; }

    .rc-demo .seen-badge {
        display: inline-block; font-family: var(--rc-font-sans); font-size: 0.65rem;
        font-weight: 600; padding: 0.1rem 0.35rem; border-radius: 3px;
        margin-left: 0.4rem; vertical-align: middle;
    }

    .rc-demo .seen-badge-a { background: var(--tx-a-bg); color: var(--tx-a); border: 1px solid var(--tx-a-border); }
    .rc-demo .seen-badge-b { background: var(--tx-b-bg); color: var(--tx-b); border: 1px solid var(--tx-b-border); }

    .rc-demo .txid-chip { display: inline-block; font-family: var(--rc-font-mono); font-size: 0.75rem; font-weight: 500; }
    .rc-demo .txid-chip.tx-old { color: var(--tx-old); }
    .rc-demo .txid-chip.tx-a-color { color: var(--tx-a); }
    .rc-demo .txid-chip.tx-b-color { color: var(--tx-b); }

    .rc-demo .committed-mark { color: var(--committed); font-size: 0.7rem; margin-left: 0.15rem; }
    .rc-demo .uncommitted-mark { color: var(--uncommitted); font-size: 0.65rem; margin-left: 0.15rem; font-style: italic; font-family: var(--rc-font-sans); }
    .rc-demo .xmax-empty { color: var(--rc-gray-300); }

    .rc-demo .explanation {
        background: var(--rc-gray-50); border: 1px solid var(--rc-gray-200);
        border-radius: var(--rc-radius); padding: 1rem 1.25rem;
        margin-bottom: 1.25rem; font-size: 0.9rem; line-height: 1.65; min-height: 3.5rem;
    }

    .rc-demo .explanation strong { color: var(--rc-gray-900); }
    .rc-demo .explanation .highlight { background: #fef9c3; padding: 0.1rem 0.3rem; border-radius: 3px; font-weight: 600; }
    .rc-demo .explanation code { font-family: var(--rc-font-mono); font-size: 0.85em; background: var(--rc-gray-100); padding: 0.1rem 0.3rem; border-radius: 3px; }

    .rc-demo .nav { display: flex; align-items: center; justify-content: center; gap: 1rem; }

    .rc-demo .nav-btn {
        display: inline-flex; align-items: center; gap: 0.35rem;
        padding: 0.5rem 1.1rem; border: 1px solid var(--rc-gray-300);
        border-radius: var(--rc-radius); background: #fff;
        font-family: var(--rc-font-sans); font-size: 0.85rem; font-weight: 500;
        color: var(--rc-gray-700); cursor: pointer;
        transition: background 0.15s, border-color 0.15s;
    }

    .rc-demo .nav-btn:hover:not(:disabled) { background: var(--rc-gray-50); border-color: var(--rc-gray-400); }
    .rc-demo .nav-btn:disabled { opacity: 0.35; cursor: default; }

    .rc-demo .step-indicator { font-size: 0.85rem; color: var(--rc-gray-500); font-weight: 500; min-width: 6rem; text-align: center; }

    .rc-demo .key-hint { text-align: center; margin-top: 0.5rem; font-size: 0.75rem; color: var(--rc-gray-400); }

    .rc-demo .key-hint kbd {
        display: inline-block; padding: 0.1rem 0.4rem; border: 1px solid var(--rc-gray-300);
        border-radius: 3px; background: var(--rc-gray-50); font-family: var(--rc-font-sans); font-size: 0.7rem;
    }

    @media (max-width: 640px) {
        .rc-demo .timelines { flex-direction: column; gap: 1rem; }
        .rc-demo .tabs { gap: 0; }
        .rc-demo .tab { padding: 0.5rem 0.6rem; font-size: 0.8rem; }
        .rc-demo .db-table { font-size: 0.75rem; }
        .rc-demo .db-table td, .rc-demo .db-table th { padding: 0.3rem 0.4rem; }
    }
&lt;/style&gt;

&lt;div class=&quot;rc-demo&quot; id=&quot;rc-demo&quot;&gt;
    &lt;div class=&quot;rc-title&quot;&gt;Read Committed: Interactive Demo&lt;/div&gt;
    &lt;p class=&quot;subtitle&quot;&gt;Step-by-step visualization of concurrent transactions and MVCC&lt;/p&gt;

    &lt;div class=&quot;tabs&quot; id=&quot;rc-tabs&quot;&gt;&lt;/div&gt;

    &lt;div class=&quot;timelines&quot;&gt;
        &lt;div class=&quot;timeline&quot;&gt;
            &lt;div class=&quot;timeline-header tx-a&quot; id=&quot;rc-header-a&quot;&gt;Transaction A&lt;/div&gt;
            &lt;div class=&quot;timeline-body tx-a&quot; id=&quot;rc-timeline-a&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;timeline&quot;&gt;
            &lt;div class=&quot;timeline-header tx-b&quot; id=&quot;rc-header-b&quot;&gt;Transaction B&lt;/div&gt;
            &lt;div class=&quot;timeline-body tx-b&quot; id=&quot;rc-timeline-b&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;div class=&quot;db-section&quot;&gt;
        &lt;div class=&quot;db-label&quot; id=&quot;rc-db-label&quot;&gt;Row Versions on Disk&lt;/div&gt;
        &lt;div class=&quot;db-table-wrap&quot;&gt;
            &lt;table class=&quot;db-table&quot; id=&quot;rc-db-table&quot;&gt;&lt;/table&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;div class=&quot;explanation&quot; id=&quot;rc-explanation&quot;&gt;&lt;/div&gt;

    &lt;div class=&quot;nav&quot;&gt;
        &lt;button class=&quot;nav-btn&quot; id=&quot;rc-btn-back&quot; disabled=&quot;&quot;&gt;&amp;#9664; Back&lt;/button&gt;
        &lt;span class=&quot;step-indicator&quot; id=&quot;rc-step-indicator&quot;&gt;Step 1 of 7&lt;/span&gt;
        &lt;button class=&quot;nav-btn&quot; id=&quot;rc-btn-fwd&quot;&gt;Forward &amp;#9654;&lt;/button&gt;
    &lt;/div&gt;
    &lt;div class=&quot;key-hint&quot;&gt;Use &lt;kbd&gt;&amp;#8592;&lt;/kbd&gt; &lt;kbd&gt;&amp;#8594;&lt;/kbd&gt; arrow keys to navigate&lt;/div&gt;

&lt;/div&gt;

&lt;script&gt;
(function() {
    var TX_OLD = 99, TX_A = 100, TX_B = 200;

    function v(xmin, xminC, xmax, xmaxC, data, seenBy, dead) {
        return { xmin: xmin, xminCommitted: xminC, xmax: xmax, xmaxCommitted: xmaxC, data: data, seenBy: seenBy || null, dead: dead || false };
    }

    var scenarios = [
        {
            id: 'non-repeatable-read', name: 'Non-repeatable Read', tableName: 'accounts',
            txAId: TX_A, txBId: TX_B, columns: ['id', 'name', 'balance'],
            steps: [
                { txA: { sql: 'BEGIN;' }, txB: null, versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000], null, false) ], explanation: 'Transaction A begins (assigned &lt;code&gt;txid 100&lt;/code&gt;). There is one row version on disk, created by an earlier committed transaction (&lt;code&gt;txid 99&lt;/code&gt;).' },
                { txA: { sql: &quot;SELECT balance FROM accounts\n  WHERE name = 'Alice';&quot;, result: '1000', resultType: 'ok' }, txB: null, versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000], 'A', false) ], explanation: 'A takes a snapshot and reads the table. The version with &lt;code&gt;xmin=99&lt;/code&gt; is committed and &lt;code&gt;xmax&lt;/code&gt; is empty, so &lt;strong&gt;A sees balance = 1000&lt;/strong&gt;.' },
                { txA: null, txB: { sql: 'BEGIN;' }, versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000], null, false) ], explanation: 'Transaction B begins (assigned &lt;code&gt;txid 200&lt;/code&gt;). Both transactions are now active concurrently.' },
                { txA: null, txB: { sql: &quot;UPDATE accounts\n  SET balance = 500\n  WHERE name = 'Alice';&quot; }, versions: [ v(TX_OLD, true, TX_B, false, [1, 'Alice', 1000], null, false), v(TX_B, false, null, null, [1, 'Alice', 500], null, false) ], explanation: 'B updates the row. PostgreSQL creates a &lt;strong&gt;new version&lt;/strong&gt;. The old version gets &lt;code&gt;xmax=200&lt;/code&gt;. The new version has &lt;code&gt;xmin=200&lt;/code&gt;. Neither is committed yet.' },
                { txA: { sql: &quot;SELECT balance FROM accounts\n  WHERE name = 'Alice';&quot;, result: '1000', resultType: 'ok' }, txB: null, versions: [ v(TX_OLD, true, TX_B, false, [1, 'Alice', 1000], 'A', false), v(TX_B, false, null, null, [1, 'Alice', 500], null, false) ], explanation: 'A takes a new snapshot. Old version: &lt;code&gt;xmin=99&lt;/code&gt; committed, &lt;code&gt;xmax=200&lt;/code&gt; not committed \u2014 row still alive. New version: &lt;code&gt;xmin=200&lt;/code&gt; not committed \u2014 invisible. &lt;strong&gt;A reads 1000&lt;/strong&gt;.' },
                { txA: null, txB: { sql: 'COMMIT;' }, versions: [ v(TX_OLD, true, TX_B, true, [1, 'Alice', 1000], null, true), v(TX_B, true, null, null, [1, 'Alice', 500], null, false) ], explanation: 'B commits. The old version is now &lt;strong&gt;dead&lt;/strong&gt;. The new version is the live version.' },
                { txA: { sql: &quot;SELECT balance FROM accounts\n  WHERE name = 'Alice';&quot;, result: '500', resultType: 'warn' }, txB: null, versions: [ v(TX_OLD, true, TX_B, true, [1, 'Alice', 1000], null, true), v(TX_B, true, null, null, [1, 'Alice', 500], 'A', false) ], explanation: 'A takes a new snapshot. &lt;strong&gt;A sees balance = 500.&lt;/strong&gt; This is a &lt;span class=&quot;highlight&quot;&gt;non-repeatable read&lt;/span&gt;: same query, different result.' }
            ]
        },
        {
            id: 'phantom-read', name: 'Phantom Read', tableName: 'orders',
            txAId: TX_A, txBId: TX_B, columns: ['id', 'product', 'status'],
            steps: [
                { txA: { sql: 'BEGIN;' }, txB: null, versions: [ v(TX_OLD, true, null, null, [1, 'Widget', 'pending'], null, false), v(TX_OLD, true, null, null, [2, 'Gadget', 'pending'], null, false), v(TX_OLD, true, null, null, [3, 'Gizmo', 'shipped'], null, false) ], explanation: 'Transaction A begins. The &lt;code&gt;orders&lt;/code&gt; table has 3 rows. Two are &lt;em&gt;pending&lt;/em&gt;, one is &lt;em&gt;shipped&lt;/em&gt;.' },
                { txA: { sql: &quot;SELECT * FROM orders\n  WHERE status = 'pending';&quot;, result: '2 rows', resultType: 'ok' }, txB: null, versions: [ v(TX_OLD, true, null, null, [1, 'Widget', 'pending'], 'A', false), v(TX_OLD, true, null, null, [2, 'Gadget', 'pending'], 'A', false), v(TX_OLD, true, null, null, [3, 'Gizmo', 'shipped'], null, false) ], explanation: 'A scans for pending orders. All versions visible. &lt;strong&gt;A gets 2 rows.&lt;/strong&gt;' },
                { txA: null, txB: { sql: 'BEGIN;' }, versions: [ v(TX_OLD, true, null, null, [1, 'Widget', 'pending'], null, false), v(TX_OLD, true, null, null, [2, 'Gadget', 'pending'], null, false), v(TX_OLD, true, null, null, [3, 'Gizmo', 'shipped'], null, false) ], explanation: 'Transaction B begins.' },
                { txA: null, txB: { sql: &quot;INSERT INTO orders\n  VALUES (4, 'Doohickey', 'pending');&quot; }, versions: [ v(TX_OLD, true, null, null, [1, 'Widget', 'pending'], null, false), v(TX_OLD, true, null, null, [2, 'Gadget', 'pending'], null, false), v(TX_OLD, true, null, null, [3, 'Gizmo', 'shipped'], null, false), v(TX_B, false, null, null, [4, 'Doohickey', 'pending'], null, false) ], explanation: 'B inserts a new row with &lt;code&gt;xmin=200&lt;/code&gt; (not yet committed).' },
                { txA: null, txB: { sql: 'COMMIT;' }, versions: [ v(TX_OLD, true, null, null, [1, 'Widget', 'pending'], null, false), v(TX_OLD, true, null, null, [2, 'Gadget', 'pending'], null, false), v(TX_OLD, true, null, null, [3, 'Gizmo', 'shipped'], null, false), v(TX_B, true, null, null, [4, 'Doohickey', 'pending'], null, false) ], explanation: 'B commits. The new row is now visible to future snapshots.' },
                { txA: { sql: &quot;SELECT * FROM orders\n  WHERE status = 'pending';&quot;, result: '3 rows', resultType: 'warn' }, txB: null, versions: [ v(TX_OLD, true, null, null, [1, 'Widget', 'pending'], 'A', false), v(TX_OLD, true, null, null, [2, 'Gadget', 'pending'], 'A', false), v(TX_OLD, true, null, null, [3, 'Gizmo', 'shipped'], null, false), v(TX_B, true, null, null, [4, 'Doohickey', 'pending'], 'A', false) ], explanation: '&lt;strong&gt;A gets 3 pending rows!&lt;/strong&gt; A row appeared that wasn\'t there before \u2014 this is a &lt;span class=&quot;highlight&quot;&gt;phantom read&lt;/span&gt;.' }
            ]
        },
        {
            id: 'dirty-read-prevention', name: 'Dirty Read Prevented', tableName: 'accounts',
            txAId: TX_A, txBId: TX_B, columns: ['id', 'name', 'balance'],
            steps: [
                { txA: { sql: 'BEGIN;' }, txB: null, versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000], null, false) ], explanation: 'Transaction A begins. Alice\'s balance is 1000.' },
                { txA: null, txB: { sql: 'BEGIN;' }, versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000], null, false) ], explanation: 'Transaction B begins.' },
                { txA: null, txB: { sql: &quot;UPDATE accounts\n  SET balance = 0\n  WHERE name = 'Alice';&quot; }, versions: [ v(TX_OLD, true, TX_B, false, [1, 'Alice', 1000], null, false), v(TX_B, false, null, null, [1, 'Alice', 0], null, false) ], explanation: 'B updates the balance to 0. Two versions exist, neither change is committed.' },
                { txA: { sql: &quot;SELECT balance FROM accounts\n  WHERE name = 'Alice';&quot;, result: '1000', resultType: 'ok' }, txB: null, versions: [ v(TX_OLD, true, TX_B, false, [1, 'Alice', 1000], 'A', false), v(TX_B, false, null, null, [1, 'Alice', 0], null, false) ], explanation: '&lt;strong&gt;A sees 1000. No dirty read!&lt;/strong&gt; The uncommitted new version is invisible to A.' },
                { txA: null, txB: { sql: 'ROLLBACK;' }, versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000], null, false) ], explanation: 'B rolls back. The new version is discarded.' },
                { txA: { sql: &quot;SELECT balance FROM accounts\n  WHERE name = 'Alice';&quot;, result: '1000', resultType: 'ok' }, txB: null, versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000], 'A', false) ], explanation: 'A reads &lt;strong&gt;1000&lt;/strong&gt;. The rolled-back change never leaked. &lt;span class=&quot;highlight&quot;&gt;Read Committed kept us safe&lt;/span&gt;.' }
            ]
        }
    ];

    var state = { scenarioIdx: 0, step: 0 };
    function cur() { return scenarios[state.scenarioIdx]; }
    function txidClass(t) { return t === TX_A ? 'tx-a-color' : t === TX_B ? 'tx-b-color' : 'tx-old'; }

    function renderTabs() {
        var c = document.getElementById('rc-tabs'); c.innerHTML = '';
        scenarios.forEach(function(s, i) {
            var b = document.createElement('button');
            b.className = 'tab' + (i === state.scenarioIdx ? ' active' : '');
            b.textContent = s.name;
            b.addEventListener('click', function() { state.scenarioIdx = i; state.step = 0; render(); });
            c.appendChild(b);
        });
    }

    function renderHeaders() {
        var s = cur();
        document.getElementById('rc-header-a').innerHTML = 'Transaction A &lt;span class=&quot;txid&quot;&gt;txid ' + s.txAId + '&lt;/span&gt;';
        document.getElementById('rc-header-b').innerHTML = 'Transaction B &lt;span class=&quot;txid&quot;&gt;txid ' + s.txBId + '&lt;/span&gt;';
    }

    function renderTimelines() {
        var s = cur();
        var aC = document.getElementById('rc-timeline-a'); aC.innerHTML = '';
        var bC = document.getElementById('rc-timeline-b'); bC.innerHTML = '';
        s.steps.forEach(function(sd, idx) {
            function mkCard(tx, side) {
                var card = document.createElement('div');
                if (tx) {
                    card.className = 'step-card';
                    if (idx &lt; state.step) card.className += ' past';
                    else if (idx === state.step) card.className += ' current-' + side;
                    else card.className += ' future';
                    var sql = document.createElement('div'); sql.className = 'sql'; sql.textContent = tx.sql; card.appendChild(sql);
                    if (tx.result &amp;&amp; idx &lt;= state.step) {
                        var r = document.createElement('div'); r.className = 'result result-' + tx.resultType; r.textContent = '\u2192 ' + tx.result; card.appendChild(r);
                    }
                } else {
                    card.className = idx &lt;= state.step ? 'step-card idle' : 'step-card future';
                    if (idx &lt;= state.step) card.textContent = '\u2014';
                }
                return card;
            }
            aC.appendChild(mkCard(sd.txA, 'a'));
            bC.appendChild(mkCard(sd.txB, 'b'));
        });
    }

    function renderTable() {
        var s = cur(), sd = s.steps[state.step];
        document.getElementById('rc-db-label').innerHTML = s.tableName + ' &lt;span&gt;\u2014 MVCC row versions on disk&lt;/span&gt;';
        var table = document.getElementById('rc-db-table'); table.innerHTML = '';
        var thead = document.createElement('thead'), hr = document.createElement('tr');
        ['xmin','xmax'].forEach(function(c){ var th=document.createElement('th'); th.className='mvcc-col'; th.textContent=c; hr.appendChild(th); });
        s.columns.forEach(function(c){ var th=document.createElement('th'); th.textContent=c; hr.appendChild(th); });
        var thV=document.createElement('th'); thV.className='mvcc-col'; thV.textContent=''; hr.appendChild(thV);
        thead.appendChild(hr); table.appendChild(thead);
        var tbody = document.createElement('tbody');
        sd.versions.forEach(function(ver) {
            var tr = document.createElement('tr'), cls = [];
            if (ver.dead) cls.push('version-dead');
            if (ver.seenBy === 'A') cls.push('version-seen-a');
            if (ver.seenBy === 'B') cls.push('version-seen-b');
            if (!ver.xminCommitted) cls.push('version-uncommitted');
            tr.className = cls.join(' ');
            // xmin
            var tdm = document.createElement('td'); tdm.className = 'mvcc-cell';
            var chip = document.createElement('span'); chip.className = 'txid-chip ' + txidClass(ver.xmin); chip.textContent = ver.xmin; tdm.appendChild(chip);
            if (ver.xminCommitted) { var m=document.createElement('span'); m.className='committed-mark'; m.textContent=' \u2714'; tdm.appendChild(m); }
            else { var m=document.createElement('span'); m.className='uncommitted-mark'; m.textContent=' pending'; tdm.appendChild(m); }
            tr.appendChild(tdm);
            // xmax
            var tdx = document.createElement('td'); tdx.className = 'mvcc-cell';
            if (ver.xmax !== null) {
                var cx=document.createElement('span'); cx.className='txid-chip '+txidClass(ver.xmax); cx.textContent=ver.xmax; tdx.appendChild(cx);
                if (ver.xmaxCommitted) { var mx=document.createElement('span'); mx.className='committed-mark'; mx.textContent=' \u2714'; tdx.appendChild(mx); }
                else { var mx=document.createElement('span'); mx.className='uncommitted-mark'; mx.textContent=' pending'; tdx.appendChild(mx); }
            } else { var em=document.createElement('span'); em.className='xmax-empty'; em.textContent='\u2014'; tdx.appendChild(em); }
            tr.appendChild(tdx);
            ver.data.forEach(function(val){ var td=document.createElement('td'); td.textContent=val; tr.appendChild(td); });
            var tdv = document.createElement('td'); tdv.className = 'mvcc-cell';
            if (ver.seenBy) { var badge=document.createElement('span'); badge.className='seen-badge seen-badge-'+ver.seenBy.toLowerCase(); badge.textContent='\u2190 '+ver.seenBy+' reads'; tdv.appendChild(badge); }
            tr.appendChild(tdv); tbody.appendChild(tr);
        });
        table.appendChild(tbody);
    }

    function renderNav() {
        var total = cur().steps.length;
        document.getElementById('rc-btn-back').disabled = state.step === 0;
        document.getElementById('rc-btn-fwd').disabled = state.step === total - 1;
        document.getElementById('rc-step-indicator').textContent = 'Step ' + (state.step + 1) + ' of ' + total;
    }

    function render() {
        renderTabs(); renderHeaders(); renderTimelines(); renderTable();
        document.getElementById('rc-explanation').innerHTML = cur().steps[state.step].explanation;
        renderNav();
    }

    document.getElementById('rc-btn-back').addEventListener('click', function() { if (state.step &gt; 0) { state.step--; render(); } });
    document.getElementById('rc-btn-fwd').addEventListener('click', function() { if (state.step &lt; cur().steps.length - 1) { state.step++; render(); } });

    var demo = document.getElementById('rc-demo');
    demo.addEventListener('keydown', function(e) {
        if (e.key === 'ArrowLeft' &amp;&amp; state.step &gt; 0) { state.step--; render(); }
        if (e.key === 'ArrowRight' &amp;&amp; state.step &lt; cur().steps.length - 1) { state.step++; render(); }
    });
    demo.setAttribute('tabindex', '0');

    render();
})();
&lt;/script&gt;

&lt;h1 id=&quot;3-isolation-level-repeatable-read&quot;&gt;3. Isolation level: Repeatable Read&lt;/h1&gt;

&lt;p&gt;With Repeatable Read, all reads within a transaction see the database as of a single snapshot (a consistent version of data). This prevents non-repeatable reads and, in PostgreSQL, also prevents phantom reads within the transaction. (The SQL standard allows phantoms under Repeatable Read, but PostgreSQL’s implementation does not.)&lt;/p&gt;

&lt;p&gt;However, this isolation level changes PostgreSQL’s behavior in an important way: some transactions may fail because PostgreSQL detects that completing them would violate the guarantees of the isolation level. This can happen, for example, when two concurrent transactions attempt to update the same row. In that case, once one transaction commits, the other may be forced to abort with a serialization-related error. The application must be prepared to decide whether the transaction can be retried.&lt;/p&gt;

&lt;p&gt;It is useful to compare this with Read Committed. Under Read Committed, concurrent updates are typically resolved by waiting: one transaction waits for the other to commit or roll back, and then proceeds. After the lock is released, the second transaction can apply its update (assuming the update conditions still match).&lt;/p&gt;

&lt;p&gt;PostgreSQL’s MVCC model also introduces a related anomaly under snapshot-based isolation: write skew. In write skew, two transactions read overlapping conditions and then perform updates that do not touch the same rows, but together violate a business rule. The database does not necessarily prevent this under Repeatable Read. This is often easiest to understand by testing practical examples.&lt;/p&gt;

&lt;style&gt;
    .rr-demo {
        --tx-a: #3b82f6;
        --tx-a-bg: #eff6ff;
        --tx-a-border: #bfdbfe;
        --tx-b: #f59e0b;
        --tx-b-bg: #fffbeb;
        --tx-b-border: #fde68a;
        --tx-old: #6b7280;
        --result-ok: #16a34a;
        --result-ok-bg: #f0fdf4;
        --result-warn: #d97706;
        --result-warn-bg: #fffbeb;
        --result-error: #dc2626;
        --result-error-bg: #fef2f2;
        --committed: #16a34a;
        --uncommitted: #9ca3af;
        --rr-gray-50: #f9fafb;
        --rr-gray-100: #f3f4f6;
        --rr-gray-200: #e5e7eb;
        --rr-gray-300: #d1d5db;
        --rr-gray-400: #9ca3af;
        --rr-gray-500: #6b7280;
        --rr-gray-600: #4b5563;
        --rr-gray-700: #374151;
        --rr-gray-800: #1f2937;
        --rr-gray-900: #111827;
        --rr-radius: 8px;
        --rr-font-mono: 'SF Mono', 'Fira Code', 'Fira Mono', Menlo, Consolas, monospace;
        --rr-font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    }

    .rr-demo {
        font-family: var(--rr-font-sans);
        color: var(--rr-gray-800);
        line-height: 1.6;
        max-width: 900px;
        margin: 2rem auto;
        padding: 1.5rem;
        border: 1px solid var(--rr-gray-200);
        border-radius: var(--rr-radius);
        background: #fff;
    }

    .rr-demo .rr-title { font-size: 1.3rem; font-weight: 700; margin-bottom: 0.25rem; color: var(--rr-gray-900); }
    .rr-demo .subtitle { color: var(--rr-gray-500); font-size: 0.95rem; margin-bottom: 1.5rem; }

    .rr-demo .tabs { display: flex; gap: 0.25rem; border-bottom: 2px solid var(--rr-gray-200); margin-bottom: 1.5rem; overflow-x: auto; }
    .rr-demo .tab { padding: 0.6rem 1rem; border: none; background: none; font-family: var(--rr-font-sans); font-size: 0.875rem; font-weight: 500; color: var(--rr-gray-500); cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -2px; white-space: nowrap; transition: color 0.15s, border-color 0.15s; }
    .rr-demo .tab:hover { color: var(--rr-gray-700); }
    .rr-demo .tab.active { color: var(--tx-a); border-bottom-color: var(--tx-a); }

    .rr-demo .timelines { display: flex; gap: 1.5rem; margin-bottom: 1.5rem; }
    .rr-demo .timeline { flex: 1; min-width: 0; }
    .rr-demo .timeline-header { font-size: 0.8rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; padding: 0.4rem 0.75rem; border-radius: var(--rr-radius) var(--rr-radius) 0 0; margin-bottom: 0; }
    .rr-demo .timeline-header .txid { font-weight: 400; opacity: 0.7; font-size: 0.75rem; text-transform: none; letter-spacing: 0; }
    .rr-demo .timeline-header.tx-a { color: var(--tx-a); background: var(--tx-a-bg); border: 1px solid var(--tx-a-border); border-bottom: none; }
    .rr-demo .timeline-header.tx-b { color: var(--tx-b); background: var(--tx-b-bg); border: 1px solid var(--tx-b-border); border-bottom: none; }
    .rr-demo .timeline-body { border-radius: 0 0 var(--rr-radius) var(--rr-radius); min-height: 120px; }
    .rr-demo .timeline-body.tx-a { border: 1px solid var(--tx-a-border); border-top: none; }
    .rr-demo .timeline-body.tx-b { border: 1px solid var(--tx-b-border); border-top: none; }

    .rr-demo .step-card { padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--rr-gray-100); transition: opacity 0.2s, background 0.2s; }
    .rr-demo .step-card:last-child { border-bottom: none; }
    .rr-demo .step-card.future { opacity: 0; pointer-events: none; height: 0; padding: 0; overflow: hidden; }
    .rr-demo .step-card.past { opacity: 0.45; }
    .rr-demo .step-card.current-a { background: var(--tx-a-bg); border-left: 3px solid var(--tx-a); opacity: 1; }
    .rr-demo .step-card.current-b { background: var(--tx-b-bg); border-left: 3px solid var(--tx-b); opacity: 1; }
    .rr-demo .step-card.idle { color: var(--rr-gray-400); font-style: italic; font-size: 0.8rem; padding: 0.35rem 0.75rem; }

    .rr-demo .sql { font-family: var(--rr-font-mono); font-size: 0.8rem; line-height: 1.5; word-break: break-word; white-space: pre-wrap; }
    .rr-demo .result { display: inline-block; font-family: var(--rr-font-mono); font-size: 0.75rem; padding: 0.15rem 0.5rem; border-radius: 4px; margin-top: 0.25rem; font-weight: 600; }
    .rr-demo .result-ok { background: var(--result-ok-bg); color: var(--result-ok); border: 1px solid #bbf7d0; }
    .rr-demo .result-warn { background: var(--result-warn-bg); color: var(--result-warn); border: 1px solid var(--tx-b-border); }
    .rr-demo .result-error { background: var(--result-error-bg); color: var(--result-error); border: 1px solid #fca5a5; }

    .rr-demo .snapshot-info { font-size: 0.8rem; padding: 0.5rem 0.75rem; background: #f0f4ff; border: 1px solid #dbeafe; border-radius: var(--rr-radius); margin-bottom: 0.75rem; color: var(--rr-gray-600); line-height: 1.5; display: none; }
    .rr-demo .snapshot-info.visible { display: block; }
    .rr-demo .snapshot-info code { font-family: var(--rr-font-mono); font-size: 0.8em; background: #e0e7ff; padding: 0.1rem 0.3rem; border-radius: 3px; }

    .rr-demo .db-section { margin-bottom: 1.25rem; }
    .rr-demo .db-label { font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--rr-gray-500); margin-bottom: 0.4rem; }
    .rr-demo .db-label span { font-weight: 400; text-transform: none; letter-spacing: 0; }
    .rr-demo .db-table-wrap { overflow-x: auto; }
    .rr-demo .db-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
    .rr-demo .db-table th { text-align: left; padding: 0.4rem 0.6rem; background: var(--rr-gray-50); border: 1px solid var(--rr-gray-200); font-weight: 600; font-size: 0.75rem; color: var(--rr-gray-600); }
    .rr-demo .db-table th.mvcc-col { background: #f0f4ff; color: var(--rr-gray-500); font-size: 0.7rem; }
    .rr-demo .db-table td { padding: 0.4rem 0.6rem; border: 1px solid var(--rr-gray-200); font-family: var(--rr-font-mono); font-size: 0.8rem; transition: background 0.3s, opacity 0.3s; }
    .rr-demo .db-table td.mvcc-cell { font-size: 0.75rem; background: #fafbff; }
    .rr-demo .db-table tr.version-dead td { opacity: 0.4; text-decoration: line-through; }
    .rr-demo .db-table tr.version-dead td.mvcc-cell { text-decoration: none; }
    .rr-demo .db-table tr.version-seen-a { outline: 2px solid var(--tx-a); outline-offset: -1px; }
    .rr-demo .db-table tr.version-seen-b { outline: 2px solid var(--tx-b); outline-offset: -1px; }
    .rr-demo .db-table tr.version-uncommitted td { border-style: dashed; }

    .rr-demo .seen-badge { display: inline-block; font-family: var(--rr-font-sans); font-size: 0.65rem; font-weight: 600; padding: 0.1rem 0.35rem; border-radius: 3px; vertical-align: middle; }
    .rr-demo .seen-badge-a { background: var(--tx-a-bg); color: var(--tx-a); border: 1px solid var(--tx-a-border); }
    .rr-demo .seen-badge-b { background: var(--tx-b-bg); color: var(--tx-b); border: 1px solid var(--tx-b-border); }
    .rr-demo .version-note { display: block; font-family: var(--rr-font-sans); font-size: 0.6rem; color: var(--rr-gray-500); font-style: italic; margin-top: 0.15rem; }

    .rr-demo .txid-chip { display: inline-block; font-family: var(--rr-font-mono); font-size: 0.75rem; font-weight: 500; }
    .rr-demo .txid-chip.tx-old { color: var(--tx-old); }
    .rr-demo .txid-chip.tx-a-color { color: var(--tx-a); }
    .rr-demo .txid-chip.tx-b-color { color: var(--tx-b); }
    .rr-demo .committed-mark { color: var(--committed); font-size: 0.7rem; margin-left: 0.15rem; }
    .rr-demo .uncommitted-mark { color: var(--uncommitted); font-size: 0.65rem; margin-left: 0.15rem; font-style: italic; font-family: var(--rr-font-sans); }
    .rr-demo .xmax-empty { color: var(--rr-gray-300); }

    .rr-demo .explanation { background: var(--rr-gray-50); border: 1px solid var(--rr-gray-200); border-radius: var(--rr-radius); padding: 1rem 1.25rem; margin-bottom: 1.25rem; font-size: 0.9rem; line-height: 1.65; min-height: 3.5rem; }
    .rr-demo .explanation strong { color: var(--rr-gray-900); }
    .rr-demo .explanation .highlight { background: #fef9c3; padding: 0.1rem 0.3rem; border-radius: 3px; font-weight: 600; }
    .rr-demo .explanation .highlight-error { background: #fef2f2; color: var(--result-error); padding: 0.1rem 0.3rem; border-radius: 3px; font-weight: 600; }
    .rr-demo .explanation code { font-family: var(--rr-font-mono); font-size: 0.85em; background: var(--rr-gray-100); padding: 0.1rem 0.3rem; border-radius: 3px; }

    .rr-demo .nav { display: flex; align-items: center; justify-content: center; gap: 1rem; }
    .rr-demo .nav-btn { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.5rem 1.1rem; border: 1px solid var(--rr-gray-300); border-radius: var(--rr-radius); background: #fff; font-family: var(--rr-font-sans); font-size: 0.85rem; font-weight: 500; color: var(--rr-gray-700); cursor: pointer; transition: background 0.15s, border-color 0.15s; }
    .rr-demo .nav-btn:hover:not(:disabled) { background: var(--rr-gray-50); border-color: var(--rr-gray-400); }
    .rr-demo .nav-btn:disabled { opacity: 0.35; cursor: default; }
    .rr-demo .step-indicator { font-size: 0.85rem; color: var(--rr-gray-500); font-weight: 500; min-width: 6rem; text-align: center; }
    .rr-demo .key-hint { text-align: center; margin-top: 0.5rem; font-size: 0.75rem; color: var(--rr-gray-400); }
    .rr-demo .key-hint kbd { display: inline-block; padding: 0.1rem 0.4rem; border: 1px solid var(--rr-gray-300); border-radius: 3px; background: var(--rr-gray-50); font-family: var(--rr-font-sans); font-size: 0.7rem; }

    @media (max-width: 640px) {
        .rr-demo .timelines { flex-direction: column; gap: 1rem; }
        .rr-demo .tabs { gap: 0; }
        .rr-demo .tab { padding: 0.5rem 0.6rem; font-size: 0.8rem; }
        .rr-demo .db-table { font-size: 0.75rem; }
        .rr-demo .db-table td, .rr-demo .db-table th { padding: 0.3rem 0.4rem; }
    }
&lt;/style&gt;

&lt;div class=&quot;rr-demo&quot; id=&quot;rr-demo&quot;&gt;
    &lt;div class=&quot;rr-title&quot;&gt;Repeatable Read: Interactive Demo&lt;/div&gt;
    &lt;p class=&quot;subtitle&quot;&gt;Snapshot isolation, serialization failures, and write skew&lt;/p&gt;

    &lt;div class=&quot;tabs&quot; id=&quot;rr-tabs&quot;&gt;&lt;/div&gt;

    &lt;div class=&quot;timelines&quot;&gt;
        &lt;div class=&quot;timeline&quot;&gt;
            &lt;div class=&quot;timeline-header tx-a&quot; id=&quot;rr-header-a&quot;&gt;Transaction A&lt;/div&gt;
            &lt;div class=&quot;timeline-body tx-a&quot; id=&quot;rr-timeline-a&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;timeline&quot;&gt;
            &lt;div class=&quot;timeline-header tx-b&quot; id=&quot;rr-header-b&quot;&gt;Transaction B&lt;/div&gt;
            &lt;div class=&quot;timeline-body tx-b&quot; id=&quot;rr-timeline-b&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;div class=&quot;snapshot-info&quot; id=&quot;rr-snapshot-info&quot;&gt;&lt;/div&gt;

    &lt;div class=&quot;db-section&quot;&gt;
        &lt;div class=&quot;db-label&quot; id=&quot;rr-db-label&quot;&gt;Row Versions on Disk&lt;/div&gt;
        &lt;div class=&quot;db-table-wrap&quot;&gt;
            &lt;table class=&quot;db-table&quot; id=&quot;rr-db-table&quot;&gt;&lt;/table&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;div class=&quot;explanation&quot; id=&quot;rr-explanation&quot;&gt;&lt;/div&gt;

    &lt;div class=&quot;nav&quot;&gt;
        &lt;button class=&quot;nav-btn&quot; id=&quot;rr-btn-back&quot; disabled=&quot;&quot;&gt;&amp;#9664; Back&lt;/button&gt;
        &lt;span class=&quot;step-indicator&quot; id=&quot;rr-step-indicator&quot;&gt;Step 1 of 7&lt;/span&gt;
        &lt;button class=&quot;nav-btn&quot; id=&quot;rr-btn-fwd&quot;&gt;Forward &amp;#9654;&lt;/button&gt;
    &lt;/div&gt;
    &lt;div class=&quot;key-hint&quot;&gt;Use &lt;kbd&gt;&amp;#8592;&lt;/kbd&gt; &lt;kbd&gt;&amp;#8594;&lt;/kbd&gt; arrow keys to navigate&lt;/div&gt;

&lt;/div&gt;

&lt;script&gt;
(function() {
    var TX_OLD = 99, TX_A = 100, TX_B = 200;

    function v(xmin, xminC, xmax, xmaxC, data, seenBy, dead, note) {
        return { xmin: xmin, xminCommitted: xminC, xmax: xmax, xmaxCommitted: xmaxC, data: data, seenBy: seenBy || null, dead: dead || false, note: note || null };
    }

    var scenarios = [
        {
            id: 'snapshot-prevents-nonrepeatable', name: 'Non-repeatable Read Prevented', tableName: 'accounts',
            txAId: TX_A, txBId: TX_B, columns: ['id', 'name', 'balance'],
            steps: [
                { txA: { sql: 'BEGIN;' }, txB: null, snapshotInfo: null, versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000]) ], explanation: 'Transaction A begins (&lt;code&gt;txid 100&lt;/code&gt;). One row version on disk: Alice with balance 1000.' },
                { txA: { sql: &quot;SELECT balance FROM accounts\n  WHERE name = 'Alice';&quot;, result: '1000', resultType: 'ok' }, txB: null, snapshotInfo: '&lt;strong&gt;A\'s snapshot taken now.&lt;/strong&gt; It records: &lt;code&gt;txid 99&lt;/code&gt; = committed. This snapshot will be reused for &lt;em&gt;every&lt;/em&gt; subsequent statement in A.', versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000], 'A') ], explanation: 'A executes its first query, which triggers a &lt;strong&gt;snapshot&lt;/strong&gt;. Unlike Read Committed (which takes a new snapshot per statement), Repeatable Read takes &lt;em&gt;one snapshot&lt;/em&gt; and reuses it for the entire transaction. A sees balance = 1000.' },
                { txA: null, txB: { sql: 'BEGIN;' }, snapshotInfo: 'A\'s snapshot (from step 2): &lt;code&gt;txid 200&lt;/code&gt; does not exist in this snapshot.', versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000]) ], explanation: 'Transaction B begins (&lt;code&gt;txid 200&lt;/code&gt;). Note: txid 200 did not exist when A took its snapshot, so A will never see anything B does.' },
                { txA: null, txB: { sql: &quot;UPDATE accounts\n  SET balance = 500\n  WHERE name = 'Alice';&quot; }, snapshotInfo: 'A\'s snapshot (from step 2): &lt;code&gt;txid 200&lt;/code&gt; is not visible.', versions: [ v(TX_OLD, true, TX_B, false, [1, 'Alice', 1000]), v(TX_B, false, null, null, [1, 'Alice', 500]) ], explanation: 'B updates the row. Old version gets &lt;code&gt;xmax=200&lt;/code&gt; (pending). New version created with &lt;code&gt;xmin=200&lt;/code&gt; (pending).' },
                { txA: null, txB: { sql: 'COMMIT;' }, snapshotInfo: 'A\'s snapshot (from step 2): &lt;code&gt;txid 200&lt;/code&gt; is &lt;strong&gt;still not visible&lt;/strong&gt; \u2014 it didn\'t exist when the snapshot was taken.', versions: [ v(TX_OLD, true, TX_B, true, [1, 'Alice', 1000], null, true), v(TX_B, true, null, null, [1, 'Alice', 500]) ], explanation: 'B commits. On disk, the old version is dead and the new version is live. But A\'s snapshot was taken &lt;em&gt;before&lt;/em&gt; txid 200 existed \u2014 A will still treat 200 as invisible.' },
                { txA: { sql: &quot;SELECT balance FROM accounts\n  WHERE name = 'Alice';&quot;, result: '1000', resultType: 'ok' }, txB: null, snapshotInfo: 'A\'s snapshot (from step 2): &lt;code&gt;txid 200&lt;/code&gt; is not visible. A sees the old version as still alive.', versions: [ v(TX_OLD, true, TX_B, true, [1, 'Alice', 1000], 'A', false, 'A\'s snapshot: xmax not visible \u2192 alive'), v(TX_B, true, null, null, [1, 'Alice', 500], null, false, 'A\'s snapshot: xmin not visible \u2192 invisible') ], explanation: '&lt;strong&gt;A still sees balance = 1000!&lt;/strong&gt; A reuses its snapshot from step 2. In that snapshot, txid 200 doesn\'t exist. &lt;span class=&quot;highlight&quot;&gt;Non-repeatable read prevented.&lt;/span&gt; Under Read Committed, A would have seen 500 here.' }
            ]
        },
        {
            id: 'serialization-failure', name: 'Serialization Failure', tableName: 'accounts',
            txAId: TX_A, txBId: TX_B, columns: ['id', 'name', 'balance'],
            steps: [
                { txA: { sql: 'BEGIN;' }, txB: null, snapshotInfo: null, versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000]) ], explanation: 'Transaction A begins. Alice has a balance of 1000.' },
                { txA: { sql: &quot;SELECT balance FROM accounts\n  WHERE name = 'Alice';&quot;, result: '1000', resultType: 'ok' }, txB: null, snapshotInfo: '&lt;strong&gt;A\'s snapshot taken now.&lt;/strong&gt; Only &lt;code&gt;txid 99&lt;/code&gt; is committed.', versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000], 'A') ], explanation: 'A reads Alice\'s balance: 1000. Snapshot taken.' },
                { txA: null, txB: { sql: 'BEGIN;' }, snapshotInfo: 'A\'s snapshot (from step 2): &lt;code&gt;txid 200&lt;/code&gt; not visible.', versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000]) ], explanation: 'Transaction B begins.' },
                { txA: null, txB: { sql: &quot;UPDATE accounts\n  SET balance = 500\n  WHERE name = 'Alice';&quot; }, snapshotInfo: 'A\'s snapshot (from step 2): &lt;code&gt;txid 200&lt;/code&gt; not visible.', versions: [ v(TX_OLD, true, TX_B, false, [1, 'Alice', 1000]), v(TX_B, false, null, null, [1, 'Alice', 500]) ], explanation: 'B updates Alice\'s balance to 500.' },
                { txA: null, txB: { sql: 'COMMIT;' }, snapshotInfo: 'A\'s snapshot (from step 2): &lt;code&gt;txid 200&lt;/code&gt; &lt;strong&gt;still not visible&lt;/strong&gt;.', versions: [ v(TX_OLD, true, TX_B, true, [1, 'Alice', 1000], null, true), v(TX_B, true, null, null, [1, 'Alice', 500]) ], explanation: 'B commits. The row has been updated on disk. But A\'s snapshot still cannot see txid 200.' },
                { txA: { sql: &quot;UPDATE accounts\n  SET balance = balance - 100\n  WHERE name = 'Alice';&quot;, result: 'ERROR: could not serialize access due to concurrent update', resultType: 'error' }, txB: null, snapshotInfo: 'A\'s snapshot (from step 2): &lt;code&gt;txid 200&lt;/code&gt; not visible. But A tried to modify a row that 200 already changed.', versions: [ v(TX_OLD, true, TX_B, true, [1, 'Alice', 1000], null, true, 'A wanted to update this version'), v(TX_B, true, null, null, [1, 'Alice', 500], null, false, 'But txid 200 already replaced it') ], explanation: 'A tries to update Alice\'s row. PostgreSQL finds the version A can see has &lt;code&gt;xmax=200&lt;/code&gt; which is now committed \u2014 someone else already modified this row after A\'s snapshot. &lt;span class=&quot;highlight-error&quot;&gt;ERROR: could not serialize access due to concurrent update.&lt;/span&gt; A must ROLLBACK and retry.' }
            ]
        },
        {
            id: 'update-succeeds', name: 'No Row Conflict', tableName: 'accounts',
            txAId: TX_A, txBId: TX_B, columns: ['id', 'name', 'balance'],
            steps: [
                { txA: { sql: 'BEGIN;' }, txB: null, snapshotInfo: null, versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000]), v(TX_OLD, true, null, null, [2, 'Bob', 2000]) ], explanation: 'Two accounts: Alice (1000) and Bob (2000). This time the transactions modify &lt;strong&gt;different rows&lt;/strong&gt;.' },
                { txA: { sql: &quot;SELECT balance FROM accounts\n  WHERE name = 'Alice';&quot;, result: '1000', resultType: 'ok' }, txB: null, snapshotInfo: '&lt;strong&gt;A\'s snapshot taken now.&lt;/strong&gt;', versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000], 'A'), v(TX_OLD, true, null, null, [2, 'Bob', 2000]) ], explanation: 'A reads Alice\'s balance: 1000. Snapshot taken.' },
                { txA: null, txB: { sql: 'BEGIN;' }, snapshotInfo: 'A\'s snapshot (from step 2): &lt;code&gt;txid 200&lt;/code&gt; not visible.', versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000]), v(TX_OLD, true, null, null, [2, 'Bob', 2000]) ], explanation: 'Transaction B begins.' },
                { txA: null, txB: { sql: &quot;UPDATE accounts\n  SET balance = 1500\n  WHERE name = 'Bob';&quot; }, snapshotInfo: 'A\'s snapshot (from step 2): &lt;code&gt;txid 200&lt;/code&gt; not visible.', versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000]), v(TX_OLD, true, TX_B, false, [2, 'Bob', 2000]), v(TX_B, false, null, null, [2, 'Bob', 1500]) ], explanation: 'B updates &lt;strong&gt;Bob\'s&lt;/strong&gt; row (not Alice\'s).' },
                { txA: null, txB: { sql: 'COMMIT;' }, snapshotInfo: 'A\'s snapshot (from step 2): &lt;code&gt;txid 200&lt;/code&gt; still not visible.', versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000]), v(TX_OLD, true, TX_B, true, [2, 'Bob', 2000], null, true), v(TX_B, true, null, null, [2, 'Bob', 1500]) ], explanation: 'B commits. Bob\'s balance is now 1500 on disk.' },
                { txA: { sql: &quot;UPDATE accounts\n  SET balance = 900\n  WHERE name = 'Alice';&quot;, result: 'UPDATE 1', resultType: 'ok' }, txB: null, snapshotInfo: 'A\'s snapshot (from step 2): &lt;code&gt;txid 200&lt;/code&gt; not visible. But Alice\'s row is untouched \u2014 no conflict.', versions: [ v(TX_OLD, true, TX_A, false, [1, 'Alice', 1000]), v(TX_A, false, null, null, [1, 'Alice', 900]), v(TX_OLD, true, TX_B, true, [2, 'Bob', 2000], null, true), v(TX_B, true, null, null, [2, 'Bob', 1500]) ], explanation: 'A updates &lt;strong&gt;Alice\'s&lt;/strong&gt; row. No one modified Alice\'s version since A\'s snapshot. &lt;strong&gt;No conflict!&lt;/strong&gt; Compare this to the Serialization Failure tab.' },
                { txA: { sql: 'COMMIT;' }, txB: null, snapshotInfo: '&lt;strong&gt;Both committed successfully.&lt;/strong&gt;', versions: [ v(TX_OLD, true, TX_A, true, [1, 'Alice', 1000], null, true), v(TX_A, true, null, null, [1, 'Alice', 900]), v(TX_OLD, true, TX_B, true, [2, 'Bob', 2000], null, true), v(TX_B, true, null, null, [2, 'Bob', 1500]) ], explanation: 'A commits. Both transactions succeeded: Alice=900, Bob=1500. The serialization failure only triggers when two transactions modify &lt;strong&gt;the same row&lt;/strong&gt;. (Note: this can lead to write skew \u2014 see the next tab.)' }
            ]
        },
        {
            id: 'write-skew', name: 'Write Skew', tableName: 'doctors',
            txAId: TX_A, txBId: TX_B, columns: ['id', 'name', 'on_call'],
            steps: [
                { txA: { sql: 'BEGIN;' }, txB: null, snapshotInfo: null, versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 'true']), v(TX_OLD, true, null, null, [2, 'Bob', 'true']) ], explanation: 'Two doctors are on call: Alice and Bob. The application requires &lt;strong&gt;at least one doctor on call&lt;/strong&gt; at all times.' },
                { txA: { sql: &quot;SELECT count(*) FROM doctors\n  WHERE on_call = true;&quot;, result: '2', resultType: 'ok' }, txB: null, snapshotInfo: '&lt;strong&gt;A\'s snapshot taken now.&lt;/strong&gt; Sees both doctors on call.', versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 'true'], 'A'), v(TX_OLD, true, null, null, [2, 'Bob', 'true'], 'A') ], explanation: 'A checks: 2 doctors on call. Alice decides she can safely go off call since Bob is still available.' },
                { txA: null, txB: { sql: 'BEGIN;' }, snapshotInfo: 'A\'s snapshot (from step 2): sees 2 doctors on call.', versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 'true']), v(TX_OLD, true, null, null, [2, 'Bob', 'true']) ], explanation: 'Transaction B begins.' },
                { txA: null, txB: { sql: &quot;SELECT count(*) FROM doctors\n  WHERE on_call = true;&quot;, result: '2', resultType: 'ok' }, snapshotInfo: 'A\'s snapshot: 2 on call. &lt;strong&gt;B\'s snapshot taken now&lt;/strong&gt;: also 2 on call.', versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 'true'], 'B'), v(TX_OLD, true, null, null, [2, 'Bob', 'true'], 'B') ], explanation: 'B checks: 2 doctors on call. Bob decides he can also go off call since Alice is still available.' },
                { txA: { sql: &quot;UPDATE doctors SET on_call = false\n  WHERE name = 'Alice';&quot; }, txB: null, snapshotInfo: 'A\'s snapshot: 2 on call. B\'s snapshot: 2 on call.', versions: [ v(TX_OLD, true, TX_A, false, [1, 'Alice', 'true']), v(TX_A, false, null, null, [1, 'Alice', 'false']), v(TX_OLD, true, null, null, [2, 'Bob', 'true']) ], explanation: 'A sets Alice off call. A modified &lt;strong&gt;row 1&lt;/strong&gt; (Alice).' },
                { txA: null, txB: { sql: &quot;UPDATE doctors SET on_call = false\n  WHERE name = 'Bob';&quot; }, snapshotInfo: 'Each modifies a &lt;strong&gt;different row&lt;/strong&gt; \u2014 no MVCC conflict.', versions: [ v(TX_OLD, true, TX_A, false, [1, 'Alice', 'true']), v(TX_A, false, null, null, [1, 'Alice', 'false']), v(TX_OLD, true, TX_B, false, [2, 'Bob', 'true']), v(TX_B, false, null, null, [2, 'Bob', 'false']) ], explanation: 'B sets Bob off call. B modified &lt;strong&gt;row 2&lt;/strong&gt; (Bob). No conflict \u2014 different tuples.' },
                { txA: { sql: 'COMMIT;' }, txB: null, snapshotInfo: 'A commits.', versions: [ v(TX_OLD, true, TX_A, true, [1, 'Alice', 'true'], null, true), v(TX_A, true, null, null, [1, 'Alice', 'false']), v(TX_OLD, true, TX_B, false, [2, 'Bob', 'true']), v(TX_B, false, null, null, [2, 'Bob', 'false']) ], explanation: 'A commits. Alice is now off call.' },
                { txA: null, txB: { sql: 'COMMIT;' }, snapshotInfo: '&lt;strong&gt;Both committed successfully.&lt;/strong&gt; No conflict was detected because they modified different rows.', versions: [ v(TX_OLD, true, TX_A, true, [1, 'Alice', 'true'], null, true), v(TX_A, true, null, null, [1, 'Alice', 'false']), v(TX_OLD, true, TX_B, true, [2, 'Bob', 'true'], null, true), v(TX_B, true, null, null, [2, 'Bob', 'false']) ], explanation: '&lt;strong&gt;B commits successfully!&lt;/strong&gt; But look: Alice &lt;code&gt;on_call=false&lt;/code&gt;, Bob &lt;code&gt;on_call=false&lt;/code&gt;. &lt;span class=&quot;highlight-error&quot;&gt;Zero doctors on call. The constraint is violated.&lt;/span&gt; This is a &lt;span class=&quot;highlight&quot;&gt;write skew&lt;/span&gt; anomaly. Only the &lt;strong&gt;Serializable&lt;/strong&gt; isolation level would prevent this.' }
            ]
        }
    ];

    var state = { scenarioIdx: 0, step: 0 };
    function cur() { return scenarios[state.scenarioIdx]; }
    function txidClass(t) { return t === TX_A ? 'tx-a-color' : t === TX_B ? 'tx-b-color' : 'tx-old'; }

    function renderTabs() {
        var c = document.getElementById('rr-tabs'); c.innerHTML = '';
        scenarios.forEach(function(s, i) {
            var b = document.createElement('button');
            b.className = 'tab' + (i === state.scenarioIdx ? ' active' : '');
            b.textContent = s.name;
            b.addEventListener('click', function() { state.scenarioIdx = i; state.step = 0; render(); });
            c.appendChild(b);
        });
    }

    function renderHeaders() {
        var s = cur();
        document.getElementById('rr-header-a').innerHTML = 'Transaction A &lt;span class=&quot;txid&quot;&gt;txid ' + s.txAId + '&lt;/span&gt;';
        document.getElementById('rr-header-b').innerHTML = 'Transaction B &lt;span class=&quot;txid&quot;&gt;txid ' + s.txBId + '&lt;/span&gt;';
    }

    function renderTimelines() {
        var s = cur();
        var aC = document.getElementById('rr-timeline-a'); aC.innerHTML = '';
        var bC = document.getElementById('rr-timeline-b'); bC.innerHTML = '';
        s.steps.forEach(function(sd, idx) {
            function mkCard(tx, side) {
                var card = document.createElement('div');
                if (tx) {
                    card.className = 'step-card';
                    if (idx &lt; state.step) card.className += ' past';
                    else if (idx === state.step) card.className += ' current-' + side;
                    else card.className += ' future';
                    var sql = document.createElement('div'); sql.className = 'sql'; sql.textContent = tx.sql; card.appendChild(sql);
                    if (tx.result &amp;&amp; idx &lt;= state.step) {
                        var r = document.createElement('div'); r.className = 'result result-' + tx.resultType; r.textContent = '\u2192 ' + tx.result; card.appendChild(r);
                    }
                } else {
                    card.className = idx &lt;= state.step ? 'step-card idle' : 'step-card future';
                    if (idx &lt;= state.step) card.textContent = '\u2014';
                }
                return card;
            }
            aC.appendChild(mkCard(sd.txA, 'a'));
            bC.appendChild(mkCard(sd.txB, 'b'));
        });
    }

    function renderSnapshot() {
        var sd = cur().steps[state.step];
        var el = document.getElementById('rr-snapshot-info');
        if (sd.snapshotInfo) { el.innerHTML = sd.snapshotInfo; el.className = 'snapshot-info visible'; }
        else { el.className = 'snapshot-info'; el.innerHTML = ''; }
    }

    function renderTable() {
        var s = cur(), sd = s.steps[state.step];
        document.getElementById('rr-db-label').innerHTML = s.tableName + ' &lt;span&gt;\u2014 MVCC row versions on disk&lt;/span&gt;';
        var table = document.getElementById('rr-db-table'); table.innerHTML = '';
        var thead = document.createElement('thead'), hr = document.createElement('tr');
        ['xmin','xmax'].forEach(function(c){ var th=document.createElement('th'); th.className='mvcc-col'; th.textContent=c; hr.appendChild(th); });
        s.columns.forEach(function(c){ var th=document.createElement('th'); th.textContent=c; hr.appendChild(th); });
        var thV=document.createElement('th'); thV.className='mvcc-col'; thV.textContent=''; hr.appendChild(thV);
        thead.appendChild(hr); table.appendChild(thead);
        var tbody = document.createElement('tbody');
        sd.versions.forEach(function(ver) {
            var tr = document.createElement('tr'), cls = [];
            if (ver.dead) cls.push('version-dead');
            if (ver.seenBy === 'A') cls.push('version-seen-a');
            if (ver.seenBy === 'B') cls.push('version-seen-b');
            if (!ver.xminCommitted) cls.push('version-uncommitted');
            tr.className = cls.join(' ');
            var tdm = document.createElement('td'); tdm.className = 'mvcc-cell';
            var chip = document.createElement('span'); chip.className = 'txid-chip ' + txidClass(ver.xmin); chip.textContent = ver.xmin; tdm.appendChild(chip);
            if (ver.xminCommitted) { var m=document.createElement('span'); m.className='committed-mark'; m.textContent=' \u2714'; tdm.appendChild(m); }
            else { var m=document.createElement('span'); m.className='uncommitted-mark'; m.textContent=' pending'; tdm.appendChild(m); }
            tr.appendChild(tdm);
            var tdx = document.createElement('td'); tdx.className = 'mvcc-cell';
            if (ver.xmax !== null) {
                var cx=document.createElement('span'); cx.className='txid-chip '+txidClass(ver.xmax); cx.textContent=ver.xmax; tdx.appendChild(cx);
                if (ver.xmaxCommitted) { var mx=document.createElement('span'); mx.className='committed-mark'; mx.textContent=' \u2714'; tdx.appendChild(mx); }
                else { var mx=document.createElement('span'); mx.className='uncommitted-mark'; mx.textContent=' pending'; tdx.appendChild(mx); }
            } else { var em=document.createElement('span'); em.className='xmax-empty'; em.textContent='\u2014'; tdx.appendChild(em); }
            tr.appendChild(tdx);
            ver.data.forEach(function(val){ var td=document.createElement('td'); td.textContent=val; tr.appendChild(td); });
            var tdv = document.createElement('td'); tdv.className = 'mvcc-cell';
            if (ver.seenBy) { var badge=document.createElement('span'); badge.className='seen-badge seen-badge-'+ver.seenBy.toLowerCase(); badge.textContent='\u2190 '+ver.seenBy+' reads'; tdv.appendChild(badge); }
            if (ver.note) { var noteEl=document.createElement('span'); noteEl.className='version-note'; noteEl.textContent=ver.note; tdv.appendChild(noteEl); }
            tr.appendChild(tdv); tbody.appendChild(tr);
        });
        table.appendChild(tbody);
    }

    function renderNav() {
        var total = cur().steps.length;
        document.getElementById('rr-btn-back').disabled = state.step === 0;
        document.getElementById('rr-btn-fwd').disabled = state.step === total - 1;
        document.getElementById('rr-step-indicator').textContent = 'Step ' + (state.step + 1) + ' of ' + total;
    }

    function render() {
        renderTabs(); renderHeaders(); renderTimelines(); renderSnapshot(); renderTable();
        document.getElementById('rr-explanation').innerHTML = cur().steps[state.step].explanation;
        renderNav();
    }

    document.getElementById('rr-btn-back').addEventListener('click', function() { if (state.step &gt; 0) { state.step--; render(); } });
    document.getElementById('rr-btn-fwd').addEventListener('click', function() { if (state.step &lt; cur().steps.length - 1) { state.step++; render(); } });

    var demo = document.getElementById('rr-demo');
    demo.addEventListener('keydown', function(e) {
        if (e.key === 'ArrowLeft' &amp;&amp; state.step &gt; 0) { state.step--; render(); }
        if (e.key === 'ArrowRight' &amp;&amp; state.step &lt; cur().steps.length - 1) { state.step++; render(); }
    });
    demo.setAttribute('tabindex', '0');

    render();
})();
&lt;/script&gt;

&lt;h1 id=&quot;4-isolation-level-serializable&quot;&gt;4. Isolation level: Serializable&lt;/h1&gt;

&lt;p&gt;The strongest isolation level is Serializable. It guarantees that concurrent transactions behave as if they were executed one-by-one in some order. In other words, the result must be equivalent to some serial execution; concurrent interleavings must not produce a different outcome.&lt;/p&gt;

&lt;p&gt;In PostgreSQL, this is implemented using Serializable Snapshot Isolation (SSI). PostgreSQL tracks read/write dependencies (including predicate-level reads) to detect when concurrency could lead to a non-serializable outcome. When such a conflict is detected, PostgreSQL aborts one of the transactions to preserve serializability.&lt;/p&gt;

&lt;p&gt;Serializable provides the strongest safety guarantees, but it is also the most expensive isolation level in terms of overhead and the likelihood of transaction retries. Just like Repeatable Read (and even more so), it requires the application to handle transaction aborts and implement a retry strategy when a serialization failure occurs.&lt;/p&gt;

&lt;style&gt;
    .sr-demo {
        --tx-a: #3b82f6;
        --tx-a-bg: #eff6ff;
        --tx-a-border: #bfdbfe;
        --tx-b: #f59e0b;
        --tx-b-bg: #fffbeb;
        --tx-b-border: #fde68a;
        --tx-old: #6b7280;
        --result-ok: #16a34a;
        --result-ok-bg: #f0fdf4;
        --result-warn: #d97706;
        --result-warn-bg: #fffbeb;
        --result-error: #dc2626;
        --result-error-bg: #fef2f2;
        --ssi: #7c3aed;
        --ssi-bg: #f5f3ff;
        --ssi-border: #ddd6fe;
        --committed: #16a34a;
        --uncommitted: #9ca3af;
        --sr-gray-50: #f9fafb;
        --sr-gray-100: #f3f4f6;
        --sr-gray-200: #e5e7eb;
        --sr-gray-300: #d1d5db;
        --sr-gray-400: #9ca3af;
        --sr-gray-500: #6b7280;
        --sr-gray-600: #4b5563;
        --sr-gray-700: #374151;
        --sr-gray-800: #1f2937;
        --sr-gray-900: #111827;
        --sr-radius: 8px;
        --sr-font-mono: 'SF Mono', 'Fira Code', 'Fira Mono', Menlo, Consolas, monospace;
        --sr-font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    }

    .sr-demo {
        font-family: var(--sr-font-sans);
        color: var(--sr-gray-800);
        line-height: 1.6;
        max-width: 900px;
        margin: 2rem auto;
        padding: 1.5rem;
        border: 1px solid var(--sr-gray-200);
        border-radius: var(--sr-radius);
        background: #fff;
    }

    .sr-demo .sr-title { font-size: 1.3rem; font-weight: 700; margin-bottom: 0.25rem; color: var(--sr-gray-900); }
    .sr-demo .subtitle { color: var(--sr-gray-500); font-size: 0.95rem; margin-bottom: 1.5rem; }

    .sr-demo .tabs { display: flex; gap: 0.25rem; border-bottom: 2px solid var(--sr-gray-200); margin-bottom: 1.5rem; overflow-x: auto; }
    .sr-demo .tab { padding: 0.6rem 1rem; border: none; background: none; font-family: var(--sr-font-sans); font-size: 0.875rem; font-weight: 500; color: var(--sr-gray-500); cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -2px; white-space: nowrap; transition: color 0.15s, border-color 0.15s; }
    .sr-demo .tab:hover { color: var(--sr-gray-700); }
    .sr-demo .tab.active { color: var(--tx-a); border-bottom-color: var(--tx-a); }

    .sr-demo .timelines { display: flex; gap: 1.5rem; margin-bottom: 1.5rem; }
    .sr-demo .timeline { flex: 1; min-width: 0; }
    .sr-demo .timeline-header { font-size: 0.8rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; padding: 0.4rem 0.75rem; border-radius: var(--sr-radius) var(--sr-radius) 0 0; margin-bottom: 0; }
    .sr-demo .timeline-header .txid { font-weight: 400; opacity: 0.7; font-size: 0.75rem; text-transform: none; letter-spacing: 0; }
    .sr-demo .timeline-header.tx-a { color: var(--tx-a); background: var(--tx-a-bg); border: 1px solid var(--tx-a-border); border-bottom: none; }
    .sr-demo .timeline-header.tx-b { color: var(--tx-b); background: var(--tx-b-bg); border: 1px solid var(--tx-b-border); border-bottom: none; }
    .sr-demo .timeline-body { border-radius: 0 0 var(--sr-radius) var(--sr-radius); min-height: 120px; }
    .sr-demo .timeline-body.tx-a { border: 1px solid var(--tx-a-border); border-top: none; }
    .sr-demo .timeline-body.tx-b { border: 1px solid var(--tx-b-border); border-top: none; }

    .sr-demo .step-card { padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--sr-gray-100); transition: opacity 0.2s, background 0.2s; }
    .sr-demo .step-card:last-child { border-bottom: none; }
    .sr-demo .step-card.future { opacity: 0; pointer-events: none; height: 0; padding: 0; overflow: hidden; }
    .sr-demo .step-card.past { opacity: 0.45; }
    .sr-demo .step-card.current-a { background: var(--tx-a-bg); border-left: 3px solid var(--tx-a); opacity: 1; }
    .sr-demo .step-card.current-b { background: var(--tx-b-bg); border-left: 3px solid var(--tx-b); opacity: 1; }
    .sr-demo .step-card.idle { color: var(--sr-gray-400); font-style: italic; font-size: 0.8rem; padding: 0.35rem 0.75rem; }

    .sr-demo .sql { font-family: var(--sr-font-mono); font-size: 0.8rem; line-height: 1.5; word-break: break-word; white-space: pre-wrap; }
    .sr-demo .result { display: inline-block; font-family: var(--sr-font-mono); font-size: 0.75rem; padding: 0.15rem 0.5rem; border-radius: 4px; margin-top: 0.25rem; font-weight: 600; }
    .sr-demo .result-ok { background: var(--result-ok-bg); color: var(--result-ok); border: 1px solid #bbf7d0; }
    .sr-demo .result-warn { background: var(--result-warn-bg); color: var(--result-warn); border: 1px solid var(--tx-b-border); }
    .sr-demo .result-error { background: var(--result-error-bg); color: var(--result-error); border: 1px solid #fca5a5; }

    .sr-demo .info-row { display: flex; gap: 0.75rem; margin-bottom: 0.75rem; }
    .sr-demo .snapshot-info, .sr-demo .ssi-info { font-size: 0.8rem; padding: 0.5rem 0.75rem; border-radius: var(--sr-radius); line-height: 1.5; display: none; flex: 1; }
    .sr-demo .snapshot-info.visible, .sr-demo .ssi-info.visible { display: block; }
    .sr-demo .snapshot-info { background: #f0f4ff; border: 1px solid #dbeafe; color: var(--sr-gray-600); }
    .sr-demo .ssi-info { background: var(--ssi-bg); border: 1px solid var(--ssi-border); color: var(--sr-gray-600); }
    .sr-demo .info-label { font-weight: 600; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em; display: block; margin-bottom: 0.2rem; }
    .sr-demo .snapshot-info .info-label { color: var(--tx-a); }
    .sr-demo .ssi-info .info-label { color: var(--ssi); }
    .sr-demo .snapshot-info code { font-family: var(--sr-font-mono); font-size: 0.8em; padding: 0.1rem 0.3rem; border-radius: 3px; background: #e0e7ff; }
    .sr-demo .ssi-info code { font-family: var(--sr-font-mono); font-size: 0.8em; padding: 0.1rem 0.3rem; border-radius: 3px; background: #ede9fe; }
    .sr-demo .dep-arrow { color: var(--ssi); font-weight: 600; }
    .sr-demo .dep-cycle { color: var(--result-error); font-weight: 700; }
    .sr-demo .dep-ok { color: var(--result-ok); font-weight: 600; }

    .sr-demo .db-section { margin-bottom: 1.25rem; }
    .sr-demo .db-label { font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--sr-gray-500); margin-bottom: 0.4rem; }
    .sr-demo .db-label span { font-weight: 400; text-transform: none; letter-spacing: 0; }
    .sr-demo .db-table-wrap { overflow-x: auto; }
    .sr-demo .db-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
    .sr-demo .db-table th { text-align: left; padding: 0.4rem 0.6rem; background: var(--sr-gray-50); border: 1px solid var(--sr-gray-200); font-weight: 600; font-size: 0.75rem; color: var(--sr-gray-600); }
    .sr-demo .db-table th.mvcc-col { background: #f0f4ff; color: var(--sr-gray-500); font-size: 0.7rem; }
    .sr-demo .db-table td { padding: 0.4rem 0.6rem; border: 1px solid var(--sr-gray-200); font-family: var(--sr-font-mono); font-size: 0.8rem; transition: background 0.3s, opacity 0.3s; }
    .sr-demo .db-table td.mvcc-cell { font-size: 0.75rem; background: #fafbff; }
    .sr-demo .db-table tr.version-dead td { opacity: 0.4; text-decoration: line-through; }
    .sr-demo .db-table tr.version-dead td.mvcc-cell { text-decoration: none; }
    .sr-demo .db-table tr.version-seen-a { outline: 2px solid var(--tx-a); outline-offset: -1px; }
    .sr-demo .db-table tr.version-seen-b { outline: 2px solid var(--tx-b); outline-offset: -1px; }
    .sr-demo .db-table tr.version-uncommitted td { border-style: dashed; }

    .sr-demo .seen-badge { display: inline-block; font-family: var(--sr-font-sans); font-size: 0.65rem; font-weight: 600; padding: 0.1rem 0.35rem; border-radius: 3px; vertical-align: middle; }
    .sr-demo .seen-badge-a { background: var(--tx-a-bg); color: var(--tx-a); border: 1px solid var(--tx-a-border); }
    .sr-demo .seen-badge-b { background: var(--tx-b-bg); color: var(--tx-b); border: 1px solid var(--tx-b-border); }
    .sr-demo .version-note { display: block; font-family: var(--sr-font-sans); font-size: 0.6rem; color: var(--sr-gray-500); font-style: italic; margin-top: 0.15rem; }

    .sr-demo .txid-chip { display: inline-block; font-family: var(--sr-font-mono); font-size: 0.75rem; font-weight: 500; }
    .sr-demo .txid-chip.tx-old { color: var(--tx-old); }
    .sr-demo .txid-chip.tx-a-color { color: var(--tx-a); }
    .sr-demo .txid-chip.tx-b-color { color: var(--tx-b); }
    .sr-demo .committed-mark { color: var(--committed); font-size: 0.7rem; margin-left: 0.15rem; }
    .sr-demo .uncommitted-mark { color: var(--uncommitted); font-size: 0.65rem; margin-left: 0.15rem; font-style: italic; font-family: var(--sr-font-sans); }
    .sr-demo .xmax-empty { color: var(--sr-gray-300); }

    .sr-demo .explanation { background: var(--sr-gray-50); border: 1px solid var(--sr-gray-200); border-radius: var(--sr-radius); padding: 1rem 1.25rem; margin-bottom: 1.25rem; font-size: 0.9rem; line-height: 1.65; min-height: 3.5rem; }
    .sr-demo .explanation strong { color: var(--sr-gray-900); }
    .sr-demo .explanation .highlight { background: #fef9c3; padding: 0.1rem 0.3rem; border-radius: 3px; font-weight: 600; }
    .sr-demo .explanation .highlight-error { background: #fef2f2; color: var(--result-error); padding: 0.1rem 0.3rem; border-radius: 3px; font-weight: 600; }
    .sr-demo .explanation .highlight-ok { background: var(--result-ok-bg); color: var(--result-ok); padding: 0.1rem 0.3rem; border-radius: 3px; font-weight: 600; }
    .sr-demo .explanation code { font-family: var(--sr-font-mono); font-size: 0.85em; background: var(--sr-gray-100); padding: 0.1rem 0.3rem; border-radius: 3px; }

    .sr-demo .nav { display: flex; align-items: center; justify-content: center; gap: 1rem; }
    .sr-demo .nav-btn { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.5rem 1.1rem; border: 1px solid var(--sr-gray-300); border-radius: var(--sr-radius); background: #fff; font-family: var(--sr-font-sans); font-size: 0.85rem; font-weight: 500; color: var(--sr-gray-700); cursor: pointer; transition: background 0.15s, border-color 0.15s; }
    .sr-demo .nav-btn:hover:not(:disabled) { background: var(--sr-gray-50); border-color: var(--sr-gray-400); }
    .sr-demo .nav-btn:disabled { opacity: 0.35; cursor: default; }
    .sr-demo .step-indicator { font-size: 0.85rem; color: var(--sr-gray-500); font-weight: 500; min-width: 6rem; text-align: center; }
    .sr-demo .key-hint { text-align: center; margin-top: 0.5rem; font-size: 0.75rem; color: var(--sr-gray-400); }
    .sr-demo .key-hint kbd { display: inline-block; padding: 0.1rem 0.4rem; border: 1px solid var(--sr-gray-300); border-radius: 3px; background: var(--sr-gray-50); font-family: var(--sr-font-sans); font-size: 0.7rem; }

    @media (max-width: 640px) {
        .sr-demo .timelines { flex-direction: column; gap: 1rem; }
        .sr-demo .info-row { flex-direction: column; gap: 0.5rem; }
        .sr-demo .tabs { gap: 0; }
        .sr-demo .tab { padding: 0.5rem 0.6rem; font-size: 0.8rem; }
        .sr-demo .db-table { font-size: 0.75rem; }
        .sr-demo .db-table td, .sr-demo .db-table th { padding: 0.3rem 0.4rem; }
    }
&lt;/style&gt;

&lt;div class=&quot;sr-demo&quot; id=&quot;sr-demo&quot;&gt;
    &lt;div class=&quot;sr-title&quot;&gt;Serializable: Interactive Demo&lt;/div&gt;
    &lt;p class=&quot;subtitle&quot;&gt;SSI predicate locking, dependency cycle detection, and true serializability&lt;/p&gt;

    &lt;div class=&quot;tabs&quot; id=&quot;sr-tabs&quot;&gt;&lt;/div&gt;

    &lt;div class=&quot;timelines&quot;&gt;
        &lt;div class=&quot;timeline&quot;&gt;
            &lt;div class=&quot;timeline-header tx-a&quot; id=&quot;sr-header-a&quot;&gt;Transaction A&lt;/div&gt;
            &lt;div class=&quot;timeline-body tx-a&quot; id=&quot;sr-timeline-a&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;timeline&quot;&gt;
            &lt;div class=&quot;timeline-header tx-b&quot; id=&quot;sr-header-b&quot;&gt;Transaction B&lt;/div&gt;
            &lt;div class=&quot;timeline-body tx-b&quot; id=&quot;sr-timeline-b&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;div class=&quot;info-row&quot;&gt;
        &lt;div class=&quot;snapshot-info&quot; id=&quot;sr-snapshot-info&quot;&gt;&lt;/div&gt;
        &lt;div class=&quot;ssi-info&quot; id=&quot;sr-ssi-info&quot;&gt;&lt;/div&gt;
    &lt;/div&gt;

    &lt;div class=&quot;db-section&quot;&gt;
        &lt;div class=&quot;db-label&quot; id=&quot;sr-db-label&quot;&gt;Row Versions on Disk&lt;/div&gt;
        &lt;div class=&quot;db-table-wrap&quot;&gt;
            &lt;table class=&quot;db-table&quot; id=&quot;sr-db-table&quot;&gt;&lt;/table&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;div class=&quot;explanation&quot; id=&quot;sr-explanation&quot;&gt;&lt;/div&gt;

    &lt;div class=&quot;nav&quot;&gt;
        &lt;button class=&quot;nav-btn&quot; id=&quot;sr-btn-back&quot; disabled=&quot;&quot;&gt;&amp;#9664; Back&lt;/button&gt;
        &lt;span class=&quot;step-indicator&quot; id=&quot;sr-step-indicator&quot;&gt;Step 1 of 8&lt;/span&gt;
        &lt;button class=&quot;nav-btn&quot; id=&quot;sr-btn-fwd&quot;&gt;Forward &amp;#9654;&lt;/button&gt;
    &lt;/div&gt;
    &lt;div class=&quot;key-hint&quot;&gt;Use &lt;kbd&gt;&amp;#8592;&lt;/kbd&gt; &lt;kbd&gt;&amp;#8594;&lt;/kbd&gt; arrow keys to navigate&lt;/div&gt;

&lt;/div&gt;

&lt;script&gt;
(function() {
    var TX_OLD = 99, TX_A = 100, TX_B = 200;

    function v(xmin, xminC, xmax, xmaxC, data, seenBy, dead, note) {
        return { xmin: xmin, xminCommitted: xminC, xmax: xmax, xmaxCommitted: xmaxC, data: data, seenBy: seenBy || null, dead: dead || false, note: note || null };
    }

    var scenarios = [
        {
            id: 'write-skew-prevented', name: 'Write Skew Prevented', tableName: 'doctors',
            txAId: TX_A, txBId: TX_B, columns: ['id', 'name', 'on_call'],
            steps: [
                { txA: { sql: 'BEGIN ISOLATION LEVEL SERIALIZABLE;' }, txB: null, snapshotInfo: null, ssiInfo: null, versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 'true']), v(TX_OLD, true, null, null, [2, 'Bob', 'true']) ], explanation: 'Two doctors are on call. The rule: &lt;strong&gt;at least one must stay on call&lt;/strong&gt;. Transaction A begins at Serializable isolation. (Under Repeatable Read, this same scenario would cause a write skew.)' },
                { txA: { sql: &quot;SELECT count(*) FROM doctors\n  WHERE on_call = true;&quot;, result: '2', resultType: 'ok' }, txB: null, snapshotInfo: '&lt;span class=&quot;info-label&quot;&gt;Snapshots&lt;/span&gt;A\'s snapshot taken. Sees both doctors on call.', ssiInfo: '&lt;span class=&quot;info-label&quot;&gt;SSI Predicate Locks&lt;/span&gt;A: SIRead lock on &lt;code&gt;on_call = true&lt;/code&gt;', versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 'true'], 'A'), v(TX_OLD, true, null, null, [2, 'Bob', 'true'], 'A') ], explanation: 'A checks: 2 on call. SSI records a &lt;strong&gt;predicate lock&lt;/strong&gt;: A has read all rows where &lt;code&gt;on_call = true&lt;/code&gt;. Any future write to matching rows will create a dependency.' },
                { txA: null, txB: { sql: 'BEGIN ISOLATION LEVEL SERIALIZABLE;' }, snapshotInfo: '&lt;span class=&quot;info-label&quot;&gt;Snapshots&lt;/span&gt;A: from step 2.', ssiInfo: '&lt;span class=&quot;info-label&quot;&gt;SSI Predicate Locks&lt;/span&gt;A: SIRead on &lt;code&gt;on_call = true&lt;/code&gt;', versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 'true']), v(TX_OLD, true, null, null, [2, 'Bob', 'true']) ], explanation: 'Transaction B begins at Serializable isolation.' },
                { txA: null, txB: { sql: &quot;SELECT count(*) FROM doctors\n  WHERE on_call = true;&quot;, result: '2', resultType: 'ok' }, snapshotInfo: '&lt;span class=&quot;info-label&quot;&gt;Snapshots&lt;/span&gt;A: from step 2. B: taken now. Both see 2 on call.', ssiInfo: '&lt;span class=&quot;info-label&quot;&gt;SSI Predicate Locks&lt;/span&gt;A: SIRead on &lt;code&gt;on_call = true&lt;/code&gt;&lt;br&gt;B: SIRead on &lt;code&gt;on_call = true&lt;/code&gt;', versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 'true'], 'B'), v(TX_OLD, true, null, null, [2, 'Bob', 'true'], 'B') ], explanation: 'B checks: 2 on call. SSI now tracks predicate locks for &lt;em&gt;both&lt;/em&gt; transactions on the same predicate.' },
                { txA: { sql: &quot;UPDATE doctors SET on_call = false\n  WHERE name = 'Alice';&quot; }, txB: null, snapshotInfo: '&lt;span class=&quot;info-label&quot;&gt;Snapshots&lt;/span&gt;A: from step 2. B: from step 4.', ssiInfo: '&lt;span class=&quot;info-label&quot;&gt;SSI Dependency Tracker&lt;/span&gt;A writes Alice (&lt;code&gt;on_call: true \u2192 false&lt;/code&gt;).&lt;br&gt;B has SIRead on &lt;code&gt;on_call = true&lt;/code&gt; which covers Alice.&lt;br&gt;&lt;span class=&quot;dep-arrow&quot;&gt;rw-dependency: B \u2192 A&lt;/span&gt; (B read data A is modifying)', versions: [ v(TX_OLD, true, TX_A, false, [1, 'Alice', 'true']), v(TX_A, false, null, null, [1, 'Alice', 'false']), v(TX_OLD, true, null, null, [2, 'Bob', 'true']) ], explanation: 'A sets Alice off call. SSI detects a &lt;strong&gt;rw-dependency from B to A&lt;/strong&gt; \u2014 B read data that A is changing.' },
                { txA: null, txB: { sql: &quot;UPDATE doctors SET on_call = false\n  WHERE name = 'Bob';&quot; }, snapshotInfo: '&lt;span class=&quot;info-label&quot;&gt;Snapshots&lt;/span&gt;A: from step 2. B: from step 4.', ssiInfo: '&lt;span class=&quot;info-label&quot;&gt;SSI Dependency Tracker&lt;/span&gt;B writes Bob (&lt;code&gt;on_call: true \u2192 false&lt;/code&gt;).&lt;br&gt;A has SIRead on &lt;code&gt;on_call = true&lt;/code&gt; which covers Bob.&lt;br&gt;&lt;span class=&quot;dep-arrow&quot;&gt;rw-dependency: A \u2192 B&lt;/span&gt; (A read data B is modifying)&lt;br&gt;&lt;span class=&quot;dep-cycle&quot;&gt;\u26a0 Cycle: A \u2192 B \u2192 A&lt;/span&gt;', versions: [ v(TX_OLD, true, TX_A, false, [1, 'Alice', 'true']), v(TX_A, false, null, null, [1, 'Alice', 'false']), v(TX_OLD, true, TX_B, false, [2, 'Bob', 'true']), v(TX_B, false, null, null, [2, 'Bob', 'false']) ], explanation: 'B sets Bob off call. SSI detects &lt;strong&gt;rw-dependency: A \u2192 B&lt;/strong&gt;. Combined with B\u2192A: &lt;span class=&quot;highlight-error&quot;&gt;cycle detected: A \u2192 B \u2192 A&lt;/span&gt;.' },
                { txA: { sql: 'COMMIT;' }, txB: null, snapshotInfo: '&lt;span class=&quot;info-label&quot;&gt;Snapshots&lt;/span&gt;A committed. B: from step 4.', ssiInfo: '&lt;span class=&quot;info-label&quot;&gt;SSI Dependency Tracker&lt;/span&gt;&lt;span class=&quot;dep-ok&quot;&gt;A committed successfully&lt;/span&gt; (first committer wins).&lt;br&gt;&lt;span class=&quot;dep-cycle&quot;&gt;Cycle A \u2192 B \u2192 A still exists. B will fail.&lt;/span&gt;', versions: [ v(TX_OLD, true, TX_A, true, [1, 'Alice', 'true'], null, true), v(TX_A, true, null, null, [1, 'Alice', 'false']), v(TX_OLD, true, TX_B, false, [2, 'Bob', 'true']), v(TX_B, false, null, null, [2, 'Bob', 'false']) ], explanation: 'A commits. The &lt;strong&gt;first committer wins&lt;/strong&gt;. The cycle still exists, so B will be rejected.' },
                { txA: null, txB: { sql: 'COMMIT;', result: 'ERROR: could not serialize access due to read/write dependencies among transactions', resultType: 'error' }, snapshotInfo: '&lt;span class=&quot;info-label&quot;&gt;Snapshots&lt;/span&gt;A: committed. B: aborted.', ssiInfo: '&lt;span class=&quot;info-label&quot;&gt;SSI Dependency Tracker&lt;/span&gt;&lt;span class=&quot;dep-cycle&quot;&gt;Cycle A \u2192 B \u2192 A \u2014 B must abort.&lt;/span&gt;&lt;br&gt;B\'s changes are discarded. B must retry.', versions: [ v(TX_OLD, true, TX_A, true, [1, 'Alice', 'true'], null, true), v(TX_A, true, null, null, [1, 'Alice', 'false']), v(TX_OLD, true, null, null, [2, 'Bob', 'true']) ], explanation: '&lt;span class=&quot;highlight-error&quot;&gt;B\'s commit is rejected!&lt;/span&gt; SSI detected the cycle and aborted B. Bob is still on call. &lt;span class=&quot;highlight&quot;&gt;Serializable prevented the anomaly that Repeatable Read allowed.&lt;/span&gt;' }
            ]
        },
        {
            id: 'rw-dependency-cycle', name: 'Dependency Cycle', tableName: 'config',
            txAId: TX_A, txBId: TX_B, columns: ['key', 'value'],
            steps: [
                { txA: { sql: 'BEGIN ISOLATION LEVEL SERIALIZABLE;' }, txB: null, snapshotInfo: null, ssiInfo: null, versions: [ v(TX_OLD, true, null, null, ['x', 1]), v(TX_OLD, true, null, null, ['y', 2]) ], explanation: 'A config table with &lt;code&gt;x=1&lt;/code&gt; and &lt;code&gt;y=2&lt;/code&gt;. A wants to copy x into y. B wants to copy y into x. In serial execution, the result is either (1,1) or (2,2) \u2014 never (2,1).' },
                { txA: { sql: &quot;SELECT value FROM config\n  WHERE key = 'x';&quot;, result: '1', resultType: 'ok' }, txB: null, snapshotInfo: '&lt;span class=&quot;info-label&quot;&gt;Snapshots&lt;/span&gt;A\'s snapshot taken.', ssiInfo: '&lt;span class=&quot;info-label&quot;&gt;SSI Predicate Locks&lt;/span&gt;A: SIRead on &lt;code&gt;key = \'x\'&lt;/code&gt;', versions: [ v(TX_OLD, true, null, null, ['x', 1], 'A'), v(TX_OLD, true, null, null, ['y', 2]) ], explanation: 'A reads &lt;code&gt;x = 1&lt;/code&gt;. SSI tracks A\'s predicate lock on &lt;code&gt;key = \'x\'&lt;/code&gt;.' },
                { txA: null, txB: { sql: 'BEGIN ISOLATION LEVEL SERIALIZABLE;' }, snapshotInfo: '&lt;span class=&quot;info-label&quot;&gt;Snapshots&lt;/span&gt;A: from step 2.', ssiInfo: '&lt;span class=&quot;info-label&quot;&gt;SSI Predicate Locks&lt;/span&gt;A: SIRead on &lt;code&gt;key = \'x\'&lt;/code&gt;', versions: [ v(TX_OLD, true, null, null, ['x', 1]), v(TX_OLD, true, null, null, ['y', 2]) ], explanation: 'Transaction B begins.' },
                { txA: null, txB: { sql: &quot;SELECT value FROM config\n  WHERE key = 'y';&quot;, result: '2', resultType: 'ok' }, snapshotInfo: '&lt;span class=&quot;info-label&quot;&gt;Snapshots&lt;/span&gt;A: from step 2. B: taken now.', ssiInfo: '&lt;span class=&quot;info-label&quot;&gt;SSI Predicate Locks&lt;/span&gt;A: SIRead on &lt;code&gt;key = \'x\'&lt;/code&gt;&lt;br&gt;B: SIRead on &lt;code&gt;key = \'y\'&lt;/code&gt;', versions: [ v(TX_OLD, true, null, null, ['x', 1]), v(TX_OLD, true, null, null, ['y', 2], 'B') ], explanation: 'B reads &lt;code&gt;y = 2&lt;/code&gt;. SSI tracks B\'s predicate lock on &lt;code&gt;key = \'y\'&lt;/code&gt;.' },
                { txA: { sql: &quot;UPDATE config SET value = 1\n  WHERE key = 'y';&quot; }, txB: null, snapshotInfo: '&lt;span class=&quot;info-label&quot;&gt;Snapshots&lt;/span&gt;A: from step 2. B: from step 4.', ssiInfo: '&lt;span class=&quot;info-label&quot;&gt;SSI Dependency Tracker&lt;/span&gt;A writes &lt;code&gt;key = \'y\'&lt;/code&gt;.&lt;br&gt;B has SIRead on &lt;code&gt;key = \'y\'&lt;/code&gt;.&lt;br&gt;&lt;span class=&quot;dep-arrow&quot;&gt;rw-dependency: B \u2192 A&lt;/span&gt; (B read y, A writes y)', versions: [ v(TX_OLD, true, null, null, ['x', 1]), v(TX_OLD, true, TX_A, false, ['y', 2]), v(TX_A, false, null, null, ['y', 1]) ], explanation: 'A writes y=1. SSI detects: &lt;strong&gt;rw-dependency: B \u2192 A&lt;/strong&gt;.' },
                { txA: null, txB: { sql: &quot;UPDATE config SET value = 2\n  WHERE key = 'x';&quot; }, snapshotInfo: '&lt;span class=&quot;info-label&quot;&gt;Snapshots&lt;/span&gt;A: from step 2. B: from step 4.', ssiInfo: '&lt;span class=&quot;info-label&quot;&gt;SSI Dependency Tracker&lt;/span&gt;B writes &lt;code&gt;key = \'x\'&lt;/code&gt;.&lt;br&gt;A has SIRead on &lt;code&gt;key = \'x\'&lt;/code&gt;.&lt;br&gt;&lt;span class=&quot;dep-arrow&quot;&gt;rw-dependency: A \u2192 B&lt;/span&gt; (A read x, B writes x)&lt;br&gt;&lt;span class=&quot;dep-cycle&quot;&gt;\u26a0 Cycle: A \u2192 B \u2192 A&lt;/span&gt;', versions: [ v(TX_OLD, true, TX_B, false, ['x', 1]), v(TX_B, false, null, null, ['x', 2]), v(TX_OLD, true, TX_A, false, ['y', 2]), v(TX_A, false, null, null, ['y', 1]) ], explanation: 'B writes x=2. SSI detects &lt;strong&gt;rw-dependency: A \u2192 B&lt;/strong&gt;. &lt;span class=&quot;highlight-error&quot;&gt;Cycle: A \u2192 B \u2192 A&lt;/span&gt;.' },
                { txA: { sql: 'COMMIT;' }, txB: null, snapshotInfo: '&lt;span class=&quot;info-label&quot;&gt;Snapshots&lt;/span&gt;A: committed. B: from step 4.', ssiInfo: '&lt;span class=&quot;info-label&quot;&gt;SSI Dependency Tracker&lt;/span&gt;&lt;span class=&quot;dep-ok&quot;&gt;A committed (first committer wins).&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;dep-cycle&quot;&gt;Cycle A \u2192 B \u2192 A \u2014 B must abort.&lt;/span&gt;', versions: [ v(TX_OLD, true, TX_B, false, ['x', 1]), v(TX_B, false, null, null, ['x', 2]), v(TX_OLD, true, TX_A, true, ['y', 2], null, true), v(TX_A, true, null, null, ['y', 1]) ], explanation: 'A commits: &lt;code&gt;y&lt;/code&gt; is now 1. First committer wins.' },
                { txA: null, txB: { sql: 'COMMIT;', result: 'ERROR: could not serialize access due to read/write dependencies among transactions', resultType: 'error' }, snapshotInfo: '&lt;span class=&quot;info-label&quot;&gt;Snapshots&lt;/span&gt;A: committed. B: aborted.', ssiInfo: '&lt;span class=&quot;info-label&quot;&gt;SSI Dependency Tracker&lt;/span&gt;&lt;span class=&quot;dep-cycle&quot;&gt;Cycle A \u2192 B \u2192 A \u2014 B aborted.&lt;/span&gt;', versions: [ v(TX_OLD, true, null, null, ['x', 1]), v(TX_OLD, true, TX_A, true, ['y', 2], null, true), v(TX_A, true, null, null, ['y', 1]) ], explanation: '&lt;span class=&quot;highlight-error&quot;&gt;B is aborted.&lt;/span&gt; Without Serializable, both would commit and produce &lt;code&gt;x=2, y=1&lt;/code&gt; (swapped) \u2014 impossible in any serial execution. B can retry and will see &lt;code&gt;x=1&lt;/code&gt;, producing &lt;code&gt;x=1, y=1&lt;/code&gt;.' }
            ]
        },
        {
            id: 'safe-concurrent', name: 'Safe Concurrent Access', tableName: 'accounts',
            txAId: TX_A, txBId: TX_B, columns: ['id', 'name', 'balance'],
            steps: [
                { txA: { sql: 'BEGIN ISOLATION LEVEL SERIALIZABLE;' }, txB: null, snapshotInfo: null, ssiInfo: null, versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000]), v(TX_OLD, true, null, null, [2, 'Bob', 2000]) ], explanation: 'Two accounts: Alice (1000) and Bob (2000). Each transaction independently withdraws from its own account. Serializable doesn\'t reject everything \u2014 let\'s see it allow safe concurrent access.' },
                { txA: { sql: &quot;SELECT balance FROM accounts\n  WHERE name = 'Alice';&quot;, result: '1000', resultType: 'ok' }, txB: null, snapshotInfo: '&lt;span class=&quot;info-label&quot;&gt;Snapshots&lt;/span&gt;A\'s snapshot taken.', ssiInfo: '&lt;span class=&quot;info-label&quot;&gt;SSI Predicate Locks&lt;/span&gt;A: SIRead on &lt;code&gt;name = \'Alice\'&lt;/code&gt;', versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000], 'A'), v(TX_OLD, true, null, null, [2, 'Bob', 2000]) ], explanation: 'A reads Alice\'s balance: 1000.' },
                { txA: { sql: &quot;UPDATE accounts SET balance = 900\n  WHERE name = 'Alice';&quot; }, txB: null, snapshotInfo: '&lt;span class=&quot;info-label&quot;&gt;Snapshots&lt;/span&gt;A: from step 2.', ssiInfo: '&lt;span class=&quot;info-label&quot;&gt;SSI Predicate Locks&lt;/span&gt;A: SIRead on &lt;code&gt;name = \'Alice\'&lt;/code&gt;, writes Alice.', versions: [ v(TX_OLD, true, TX_A, false, [1, 'Alice', 1000]), v(TX_A, false, null, null, [1, 'Alice', 900]), v(TX_OLD, true, null, null, [2, 'Bob', 2000]) ], explanation: 'A withdraws 100 from Alice. No cross-row dependency.' },
                { txA: null, txB: { sql: 'BEGIN ISOLATION LEVEL SERIALIZABLE;' }, snapshotInfo: '&lt;span class=&quot;info-label&quot;&gt;Snapshots&lt;/span&gt;A: from step 2.', ssiInfo: '&lt;span class=&quot;info-label&quot;&gt;SSI Predicate Locks&lt;/span&gt;A: SIRead on &lt;code&gt;name = \'Alice\'&lt;/code&gt;, writes Alice.', versions: [ v(TX_OLD, true, TX_A, false, [1, 'Alice', 1000]), v(TX_A, false, null, null, [1, 'Alice', 900]), v(TX_OLD, true, null, null, [2, 'Bob', 2000]) ], explanation: 'Transaction B begins.' },
                { txA: null, txB: { sql: &quot;SELECT balance FROM accounts\n  WHERE name = 'Bob';&quot;, result: '2000', resultType: 'ok' }, snapshotInfo: '&lt;span class=&quot;info-label&quot;&gt;Snapshots&lt;/span&gt;A: from step 2. B: taken now.', ssiInfo: '&lt;span class=&quot;info-label&quot;&gt;SSI Predicate Locks&lt;/span&gt;A: SIRead on &lt;code&gt;name = \'Alice\'&lt;/code&gt;, writes Alice.&lt;br&gt;B: SIRead on &lt;code&gt;name = \'Bob\'&lt;/code&gt;&lt;br&gt;&lt;span class=&quot;dep-ok&quot;&gt;No conflicts \u2014 different predicates, different rows.&lt;/span&gt;', versions: [ v(TX_OLD, true, TX_A, false, [1, 'Alice', 1000]), v(TX_A, false, null, null, [1, 'Alice', 900]), v(TX_OLD, true, null, null, [2, 'Bob', 2000], 'B') ], explanation: 'B reads Bob\'s balance: 2000. &lt;strong&gt;No overlap&lt;/strong&gt; with A\'s locks.' },
                { txA: null, txB: { sql: &quot;UPDATE accounts SET balance = 1900\n  WHERE name = 'Bob';&quot; }, snapshotInfo: '&lt;span class=&quot;info-label&quot;&gt;Snapshots&lt;/span&gt;A: from step 2. B: from step 5.', ssiInfo: '&lt;span class=&quot;info-label&quot;&gt;SSI Dependency Tracker&lt;/span&gt;A: reads Alice, writes Alice.&lt;br&gt;B: reads Bob, writes Bob.&lt;br&gt;&lt;span class=&quot;dep-ok&quot;&gt;No rw-dependencies between A and B. No cycle possible.&lt;/span&gt;', versions: [ v(TX_OLD, true, TX_A, false, [1, 'Alice', 1000]), v(TX_A, false, null, null, [1, 'Alice', 900]), v(TX_OLD, true, TX_B, false, [2, 'Bob', 2000]), v(TX_B, false, null, null, [2, 'Bob', 1900]) ], explanation: 'B withdraws 100 from Bob. No cross-dependencies exist.' },
                { txA: { sql: 'COMMIT;' }, txB: null, snapshotInfo: '&lt;span class=&quot;info-label&quot;&gt;Snapshots&lt;/span&gt;A: committed. B: from step 5.', ssiInfo: '&lt;span class=&quot;info-label&quot;&gt;SSI Dependency Tracker&lt;/span&gt;&lt;span class=&quot;dep-ok&quot;&gt;A committed. No cycles detected.&lt;/span&gt;', versions: [ v(TX_OLD, true, TX_A, true, [1, 'Alice', 1000], null, true), v(TX_A, true, null, null, [1, 'Alice', 900]), v(TX_OLD, true, TX_B, false, [2, 'Bob', 2000]), v(TX_B, false, null, null, [2, 'Bob', 1900]) ], explanation: 'A commits successfully. No dependency cycle.' },
                { txA: null, txB: { sql: 'COMMIT;' }, snapshotInfo: '&lt;span class=&quot;info-label&quot;&gt;Snapshots&lt;/span&gt;A: committed. B: committed.', ssiInfo: '&lt;span class=&quot;info-label&quot;&gt;SSI Dependency Tracker&lt;/span&gt;&lt;span class=&quot;dep-ok&quot;&gt;B committed. No cycles. Both transactions succeeded.&lt;/span&gt;', versions: [ v(TX_OLD, true, TX_A, true, [1, 'Alice', 1000], null, true), v(TX_A, true, null, null, [1, 'Alice', 900]), v(TX_OLD, true, TX_B, true, [2, 'Bob', 2000], null, true), v(TX_B, true, null, null, [2, 'Bob', 1900]) ], explanation: '&lt;span class=&quot;highlight-ok&quot;&gt;B commits successfully!&lt;/span&gt; Alice: 900, Bob: 1900. Serializable is not overly restrictive \u2014 it only rejects transactions when it detects a genuine dependency cycle.' }
            ]
        }
    ];

    var state = { scenarioIdx: 0, step: 0 };
    function cur() { return scenarios[state.scenarioIdx]; }
    function txidClass(t) { return t === TX_A ? 'tx-a-color' : t === TX_B ? 'tx-b-color' : 'tx-old'; }

    function renderTabs() {
        var c = document.getElementById('sr-tabs'); c.innerHTML = '';
        scenarios.forEach(function(s, i) {
            var b = document.createElement('button');
            b.className = 'tab' + (i === state.scenarioIdx ? ' active' : '');
            b.textContent = s.name;
            b.addEventListener('click', function() { state.scenarioIdx = i; state.step = 0; render(); });
            c.appendChild(b);
        });
    }

    function renderHeaders() {
        var s = cur();
        document.getElementById('sr-header-a').innerHTML = 'Transaction A &lt;span class=&quot;txid&quot;&gt;txid ' + s.txAId + '&lt;/span&gt;';
        document.getElementById('sr-header-b').innerHTML = 'Transaction B &lt;span class=&quot;txid&quot;&gt;txid ' + s.txBId + '&lt;/span&gt;';
    }

    function renderTimelines() {
        var s = cur();
        var aC = document.getElementById('sr-timeline-a'); aC.innerHTML = '';
        var bC = document.getElementById('sr-timeline-b'); bC.innerHTML = '';
        s.steps.forEach(function(sd, idx) {
            function mkCard(tx, side) {
                var card = document.createElement('div');
                if (tx) {
                    card.className = 'step-card';
                    if (idx &lt; state.step) card.className += ' past';
                    else if (idx === state.step) card.className += ' current-' + side;
                    else card.className += ' future';
                    var sql = document.createElement('div'); sql.className = 'sql'; sql.textContent = tx.sql; card.appendChild(sql);
                    if (tx.result &amp;&amp; idx &lt;= state.step) {
                        var r = document.createElement('div'); r.className = 'result result-' + tx.resultType; r.textContent = '\u2192 ' + tx.result; card.appendChild(r);
                    }
                } else {
                    card.className = idx &lt;= state.step ? 'step-card idle' : 'step-card future';
                    if (idx &lt;= state.step) card.textContent = '\u2014';
                }
                return card;
            }
            aC.appendChild(mkCard(sd.txA, 'a'));
            bC.appendChild(mkCard(sd.txB, 'b'));
        });
    }

    function renderInfoBanners() {
        var sd = cur().steps[state.step];
        var snapEl = document.getElementById('sr-snapshot-info');
        var ssiEl = document.getElementById('sr-ssi-info');
        if (sd.snapshotInfo) { snapEl.innerHTML = sd.snapshotInfo; snapEl.className = 'snapshot-info visible'; }
        else { snapEl.className = 'snapshot-info'; snapEl.innerHTML = ''; }
        if (sd.ssiInfo) { ssiEl.innerHTML = sd.ssiInfo; ssiEl.className = 'ssi-info visible'; }
        else { ssiEl.className = 'ssi-info'; ssiEl.innerHTML = ''; }
    }

    function renderTable() {
        var s = cur(), sd = s.steps[state.step];
        document.getElementById('sr-db-label').innerHTML = s.tableName + ' &lt;span&gt;\u2014 MVCC row versions on disk&lt;/span&gt;';
        var table = document.getElementById('sr-db-table'); table.innerHTML = '';
        var thead = document.createElement('thead'), hr = document.createElement('tr');
        ['xmin','xmax'].forEach(function(c){ var th=document.createElement('th'); th.className='mvcc-col'; th.textContent=c; hr.appendChild(th); });
        s.columns.forEach(function(c){ var th=document.createElement('th'); th.textContent=c; hr.appendChild(th); });
        var thV=document.createElement('th'); thV.className='mvcc-col'; thV.textContent=''; hr.appendChild(thV);
        thead.appendChild(hr); table.appendChild(thead);
        var tbody = document.createElement('tbody');
        sd.versions.forEach(function(ver) {
            var tr = document.createElement('tr'), cls = [];
            if (ver.dead) cls.push('version-dead');
            if (ver.seenBy === 'A') cls.push('version-seen-a');
            if (ver.seenBy === 'B') cls.push('version-seen-b');
            if (!ver.xminCommitted) cls.push('version-uncommitted');
            tr.className = cls.join(' ');
            var tdm = document.createElement('td'); tdm.className = 'mvcc-cell';
            var chip = document.createElement('span'); chip.className = 'txid-chip ' + txidClass(ver.xmin); chip.textContent = ver.xmin; tdm.appendChild(chip);
            if (ver.xminCommitted) { var m=document.createElement('span'); m.className='committed-mark'; m.textContent=' \u2714'; tdm.appendChild(m); }
            else { var m=document.createElement('span'); m.className='uncommitted-mark'; m.textContent=' pending'; tdm.appendChild(m); }
            tr.appendChild(tdm);
            var tdx = document.createElement('td'); tdx.className = 'mvcc-cell';
            if (ver.xmax !== null) {
                var cx=document.createElement('span'); cx.className='txid-chip '+txidClass(ver.xmax); cx.textContent=ver.xmax; tdx.appendChild(cx);
                if (ver.xmaxCommitted) { var mx=document.createElement('span'); mx.className='committed-mark'; mx.textContent=' \u2714'; tdx.appendChild(mx); }
                else { var mx=document.createElement('span'); mx.className='uncommitted-mark'; mx.textContent=' pending'; tdx.appendChild(mx); }
            } else { var em=document.createElement('span'); em.className='xmax-empty'; em.textContent='\u2014'; tdx.appendChild(em); }
            tr.appendChild(tdx);
            ver.data.forEach(function(val){ var td=document.createElement('td'); td.textContent=val; tr.appendChild(td); });
            var tdv = document.createElement('td'); tdv.className = 'mvcc-cell';
            if (ver.seenBy) { var badge=document.createElement('span'); badge.className='seen-badge seen-badge-'+ver.seenBy.toLowerCase(); badge.textContent='\u2190 '+ver.seenBy+' reads'; tdv.appendChild(badge); }
            if (ver.note) { var noteEl=document.createElement('span'); noteEl.className='version-note'; noteEl.textContent=ver.note; tdv.appendChild(noteEl); }
            tr.appendChild(tdv); tbody.appendChild(tr);
        });
        table.appendChild(tbody);
    }

    function renderNav() {
        var total = cur().steps.length;
        document.getElementById('sr-btn-back').disabled = state.step === 0;
        document.getElementById('sr-btn-fwd').disabled = state.step === total - 1;
        document.getElementById('sr-step-indicator').textContent = 'Step ' + (state.step + 1) + ' of ' + total;
    }

    function render() {
        renderTabs(); renderHeaders(); renderTimelines(); renderInfoBanners(); renderTable();
        document.getElementById('sr-explanation').innerHTML = cur().steps[state.step].explanation;
        renderNav();
    }

    document.getElementById('sr-btn-back').addEventListener('click', function() { if (state.step &gt; 0) { state.step--; render(); } });
    document.getElementById('sr-btn-fwd').addEventListener('click', function() { if (state.step &lt; cur().steps.length - 1) { state.step++; render(); } });

    var demo = document.getElementById('sr-demo');
    demo.addEventListener('keydown', function(e) {
        if (e.key === 'ArrowLeft' &amp;&amp; state.step &gt; 0) { state.step--; render(); }
        if (e.key === 'ArrowRight' &amp;&amp; state.step &lt; cur().steps.length - 1) { state.step++; render(); }
    });
    demo.setAttribute('tabindex', '0');

    render();
})();
&lt;/script&gt;

&lt;h1 id=&quot;interlude&quot;&gt;Interlude&lt;/h1&gt;

&lt;p&gt;Hopefully, the sections above explain why developers need to understand the different transaction isolation levels—and when it makes sense to use each of them.&lt;/p&gt;

&lt;p&gt;At this point, it’s worth returning once more to the earlier discussion about Read Committed. There are practical techniques that let us keep using Read Committed while still avoiding many of its common pitfalls.&lt;/p&gt;

&lt;h1 id=&quot;read-committed-and-locks&quot;&gt;Read Committed and locks&lt;/h1&gt;

&lt;p&gt;You might be getting a little tired of new concepts by now, but PostgreSQL gives us several tools that can help us avoid moving to higher isolation levels—and, importantly, avoid the transaction aborts that stronger levels (Repeatable Read / Serializable) may require you to handle with retry logic.&lt;/p&gt;

&lt;p&gt;If we want to ensure that data we read cannot change during our transaction, we can add a locking clause to the query, such as FOR SHARE.&lt;/p&gt;

&lt;p&gt;This prevents the selected rows from being modified while the lock is held. Any concurrent transaction that tries to update or delete those rows must wait until the current transaction completes.&lt;/p&gt;

&lt;p&gt;A similar approach is FOR UPDATE, which takes a stronger lock. In addition to blocking updates and deletes, it also blocks other transactions from acquiring certain weaker locks on the same rows (including FOR SHARE-type locks). In other words: FOR UPDATE is more restrictive, and should be used only when you truly intend to update the locked rows (or when you intentionally want to serialize access).&lt;/p&gt;

&lt;p&gt;Locks can also be used to prevent phantom reads, but only under specific access patterns. If two transactions protect a shared “guard row” (or some other shared resource) using SELECT … FOR UPDATE, then inserts/updates that would otherwise create phantoms can be forced to wait—effectively serializing the critical section.&lt;/p&gt;

&lt;p&gt;It’s also possible to use advisory locks to coordinate application-level critical sections. Advisory locks are powerful, but since they require careful design and consistent usage across the codebase, I’ll leave them as optional further reading.&lt;/p&gt;

&lt;p&gt;To better understand how locks behave in practice, you can follow their effects in the interactive simulator below.&lt;/p&gt;

&lt;style&gt;
    .lk-demo {
        --tx-a: #3b82f6;
        --tx-a-bg: #eff6ff;
        --tx-a-border: #bfdbfe;
        --tx-b: #f59e0b;
        --tx-b-bg: #fffbeb;
        --tx-b-border: #fde68a;
        --tx-old: #6b7280;
        --result-ok: #16a34a;
        --result-ok-bg: #f0fdf4;
        --result-warn: #d97706;
        --result-warn-bg: #fffbeb;
        --result-error: #dc2626;
        --result-error-bg: #fef2f2;
        --result-blocked: #c2410c;
        --result-blocked-bg: #fff7ed;
        --lock: #0d9488;
        --lock-bg: #f0fdfa;
        --lock-border: #99f6e4;
        --committed: #16a34a;
        --uncommitted: #9ca3af;
        --lk-gray-50: #f9fafb;
        --lk-gray-100: #f3f4f6;
        --lk-gray-200: #e5e7eb;
        --lk-gray-300: #d1d5db;
        --lk-gray-400: #9ca3af;
        --lk-gray-500: #6b7280;
        --lk-gray-600: #4b5563;
        --lk-gray-700: #374151;
        --lk-gray-800: #1f2937;
        --lk-gray-900: #111827;
        --lk-radius: 8px;
        --lk-font-mono: 'SF Mono', 'Fira Code', 'Fira Mono', Menlo, Consolas, monospace;
        --lk-font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    }

    .lk-demo {
        font-family: var(--lk-font-sans);
        color: var(--lk-gray-800);
        line-height: 1.6;
        max-width: 900px;
        margin: 2rem auto;
        padding: 1.5rem;
        border: 1px solid var(--lk-gray-200);
        border-radius: var(--lk-radius);
        background: #fff;
    }

    .lk-demo .lk-title { font-size: 1.3rem; font-weight: 700; margin-bottom: 0.25rem; color: var(--lk-gray-900); }
    .lk-demo .subtitle { color: var(--lk-gray-500); font-size: 0.95rem; margin-bottom: 1.5rem; }

    .lk-demo .tabs { display: flex; gap: 0.25rem; border-bottom: 2px solid var(--lk-gray-200); margin-bottom: 1.5rem; overflow-x: auto; }
    .lk-demo .tab { padding: 0.6rem 1rem; border: none; background: none; font-family: var(--lk-font-sans); font-size: 0.875rem; font-weight: 500; color: var(--lk-gray-500); cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -2px; white-space: nowrap; transition: color 0.15s, border-color 0.15s; }
    .lk-demo .tab:hover { color: var(--lk-gray-700); }
    .lk-demo .tab.active { color: var(--tx-a); border-bottom-color: var(--tx-a); }

    .lk-demo .timelines { display: flex; gap: 1.5rem; margin-bottom: 1.5rem; }
    .lk-demo .timeline { flex: 1; min-width: 0; }
    .lk-demo .timeline-header { font-size: 0.8rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; padding: 0.4rem 0.75rem; border-radius: var(--lk-radius) var(--lk-radius) 0 0; margin-bottom: 0; }
    .lk-demo .timeline-header .txid { font-weight: 400; opacity: 0.7; font-size: 0.75rem; text-transform: none; letter-spacing: 0; }
    .lk-demo .timeline-header.tx-a { color: var(--tx-a); background: var(--tx-a-bg); border: 1px solid var(--tx-a-border); border-bottom: none; }
    .lk-demo .timeline-header.tx-b { color: var(--tx-b); background: var(--tx-b-bg); border: 1px solid var(--tx-b-border); border-bottom: none; }
    .lk-demo .timeline-body { border-radius: 0 0 var(--lk-radius) var(--lk-radius); min-height: 120px; }
    .lk-demo .timeline-body.tx-a { border: 1px solid var(--tx-a-border); border-top: none; }
    .lk-demo .timeline-body.tx-b { border: 1px solid var(--tx-b-border); border-top: none; }

    .lk-demo .step-card { padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--lk-gray-100); transition: opacity 0.2s, background 0.2s; }
    .lk-demo .step-card:last-child { border-bottom: none; }
    .lk-demo .step-card.future { opacity: 0; pointer-events: none; height: 0; padding: 0; overflow: hidden; }
    .lk-demo .step-card.past { opacity: 0.45; }
    .lk-demo .step-card.current-a { background: var(--tx-a-bg); border-left: 3px solid var(--tx-a); opacity: 1; }
    .lk-demo .step-card.current-b { background: var(--tx-b-bg); border-left: 3px solid var(--tx-b); opacity: 1; }
    .lk-demo .step-card.idle { color: var(--lk-gray-400); font-style: italic; font-size: 0.8rem; padding: 0.35rem 0.75rem; }

    .lk-demo .sql { font-family: var(--lk-font-mono); font-size: 0.8rem; line-height: 1.5; word-break: break-word; white-space: pre-wrap; }
    .lk-demo .result { display: inline-block; font-family: var(--lk-font-mono); font-size: 0.75rem; padding: 0.15rem 0.5rem; border-radius: 4px; margin-top: 0.25rem; font-weight: 600; }
    .lk-demo .result-ok { background: var(--result-ok-bg); color: var(--result-ok); border: 1px solid #bbf7d0; }
    .lk-demo .result-warn { background: var(--result-warn-bg); color: var(--result-warn); border: 1px solid var(--tx-b-border); }
    .lk-demo .result-error { background: var(--result-error-bg); color: var(--result-error); border: 1px solid #fca5a5; }
    .lk-demo .result-blocked { background: var(--result-blocked-bg); color: var(--result-blocked); border: 1px dashed #fdba74; animation: lk-pulse-blocked 2s infinite; }
    @keyframes lk-pulse-blocked { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }

    .lk-demo .lock-info { font-size: 0.8rem; padding: 0.5rem 0.75rem; background: var(--lock-bg); border: 1px solid var(--lock-border); border-radius: var(--lk-radius); margin-bottom: 0.75rem; color: var(--lk-gray-600); line-height: 1.5; display: none; }
    .lk-demo .lock-info.visible { display: block; }
    .lk-demo .lock-info .info-label { font-weight: 600; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em; display: block; margin-bottom: 0.2rem; color: var(--lock); }
    .lk-demo .lock-info code { font-family: var(--lk-font-mono); font-size: 0.8em; background: #ccfbf1; padding: 0.1rem 0.3rem; border-radius: 3px; }
    .lk-demo .lock-held { color: var(--lock); font-weight: 600; }
    .lk-demo .lock-waiting { color: var(--result-blocked); font-weight: 600; }
    .lk-demo .lock-released { color: var(--lk-gray-400); }

    .lk-demo .db-section { margin-bottom: 1.25rem; }
    .lk-demo .db-label { font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--lk-gray-500); margin-bottom: 0.4rem; }
    .lk-demo .db-label span { font-weight: 400; text-transform: none; letter-spacing: 0; }
    .lk-demo .db-table-wrap { overflow-x: auto; }
    .lk-demo .db-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
    .lk-demo .db-table th { text-align: left; padding: 0.4rem 0.6rem; background: var(--lk-gray-50); border: 1px solid var(--lk-gray-200); font-weight: 600; font-size: 0.75rem; color: var(--lk-gray-600); }
    .lk-demo .db-table th.mvcc-col { background: #f0f4ff; color: var(--lk-gray-500); font-size: 0.7rem; }
    .lk-demo .db-table td { padding: 0.4rem 0.6rem; border: 1px solid var(--lk-gray-200); font-family: var(--lk-font-mono); font-size: 0.8rem; transition: background 0.3s, opacity 0.3s; }
    .lk-demo .db-table td.mvcc-cell { font-size: 0.75rem; background: #fafbff; }
    .lk-demo .db-table tr.version-dead td { opacity: 0.4; text-decoration: line-through; }
    .lk-demo .db-table tr.version-dead td.mvcc-cell { text-decoration: none; }
    .lk-demo .db-table tr.version-seen-a { outline: 2px solid var(--tx-a); outline-offset: -1px; }
    .lk-demo .db-table tr.version-seen-b { outline: 2px solid var(--tx-b); outline-offset: -1px; }
    .lk-demo .db-table tr.version-uncommitted td { border-style: dashed; }

    .lk-demo .seen-badge { display: inline-block; font-family: var(--lk-font-sans); font-size: 0.65rem; font-weight: 600; padding: 0.1rem 0.35rem; border-radius: 3px; vertical-align: middle; }
    .lk-demo .seen-badge-a { background: var(--tx-a-bg); color: var(--tx-a); border: 1px solid var(--tx-a-border); }
    .lk-demo .seen-badge-b { background: var(--tx-b-bg); color: var(--tx-b); border: 1px solid var(--tx-b-border); }
    .lk-demo .lock-badge { display: inline-block; font-family: var(--lk-font-sans); font-size: 0.6rem; font-weight: 600; padding: 0.1rem 0.35rem; border-radius: 3px; vertical-align: middle; margin-left: 0.3rem; background: var(--lock-bg); color: var(--lock); border: 1px solid var(--lock-border); }
    .lk-demo .lock-badge-waiting { background: var(--result-blocked-bg); color: var(--result-blocked); border: 1px dashed #fdba74; }
    .lk-demo .version-note { display: block; font-family: var(--lk-font-sans); font-size: 0.6rem; color: var(--lk-gray-500); font-style: italic; margin-top: 0.15rem; }

    .lk-demo .txid-chip { display: inline-block; font-family: var(--lk-font-mono); font-size: 0.75rem; font-weight: 500; }
    .lk-demo .txid-chip.tx-old { color: var(--tx-old); }
    .lk-demo .txid-chip.tx-a-color { color: var(--tx-a); }
    .lk-demo .txid-chip.tx-b-color { color: var(--tx-b); }
    .lk-demo .committed-mark { color: var(--committed); font-size: 0.7rem; margin-left: 0.15rem; }
    .lk-demo .uncommitted-mark { color: var(--uncommitted); font-size: 0.65rem; margin-left: 0.15rem; font-style: italic; font-family: var(--lk-font-sans); }
    .lk-demo .xmax-empty { color: var(--lk-gray-300); }

    .lk-demo .explanation { background: var(--lk-gray-50); border: 1px solid var(--lk-gray-200); border-radius: var(--lk-radius); padding: 1rem 1.25rem; margin-bottom: 1.25rem; font-size: 0.9rem; line-height: 1.65; min-height: 3.5rem; }
    .lk-demo .explanation strong { color: var(--lk-gray-900); }
    .lk-demo .explanation .highlight { background: #fef9c3; padding: 0.1rem 0.3rem; border-radius: 3px; font-weight: 600; }
    .lk-demo .explanation .highlight-ok { background: var(--result-ok-bg); color: var(--result-ok); padding: 0.1rem 0.3rem; border-radius: 3px; font-weight: 600; }
    .lk-demo .explanation code { font-family: var(--lk-font-mono); font-size: 0.85em; background: var(--lk-gray-100); padding: 0.1rem 0.3rem; border-radius: 3px; }

    .lk-demo .nav { display: flex; align-items: center; justify-content: center; gap: 1rem; }
    .lk-demo .nav-btn { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.5rem 1.1rem; border: 1px solid var(--lk-gray-300); border-radius: var(--lk-radius); background: #fff; font-family: var(--lk-font-sans); font-size: 0.85rem; font-weight: 500; color: var(--lk-gray-700); cursor: pointer; transition: background 0.15s, border-color 0.15s; }
    .lk-demo .nav-btn:hover:not(:disabled) { background: var(--lk-gray-50); border-color: var(--lk-gray-400); }
    .lk-demo .nav-btn:disabled { opacity: 0.35; cursor: default; }
    .lk-demo .step-indicator { font-size: 0.85rem; color: var(--lk-gray-500); font-weight: 500; min-width: 6rem; text-align: center; }
    .lk-demo .key-hint { text-align: center; margin-top: 0.5rem; font-size: 0.75rem; color: var(--lk-gray-400); }
    .lk-demo .key-hint kbd { display: inline-block; padding: 0.1rem 0.4rem; border: 1px solid var(--lk-gray-300); border-radius: 3px; background: var(--lk-gray-50); font-family: var(--lk-font-sans); font-size: 0.7rem; }

    @media (max-width: 640px) {
        .lk-demo .timelines { flex-direction: column; gap: 1rem; }
        .lk-demo .tabs { gap: 0; }
        .lk-demo .tab { padding: 0.5rem 0.6rem; font-size: 0.8rem; }
        .lk-demo .db-table { font-size: 0.75rem; }
        .lk-demo .db-table td, .lk-demo .db-table th { padding: 0.3rem 0.4rem; }
    }
&lt;/style&gt;

&lt;div class=&quot;lk-demo&quot; id=&quot;lk-demo&quot;&gt;
    &lt;div class=&quot;lk-title&quot;&gt;Read Committed: Avoiding Anomalies with Explicit Locks&lt;/div&gt;
    &lt;p class=&quot;subtitle&quot;&gt;Using FOR SHARE and FOR UPDATE to get stronger guarantees without changing isolation level&lt;/p&gt;

    &lt;div class=&quot;tabs&quot; id=&quot;lk-tabs&quot;&gt;&lt;/div&gt;

    &lt;div class=&quot;timelines&quot;&gt;
        &lt;div class=&quot;timeline&quot;&gt;
            &lt;div class=&quot;timeline-header tx-a&quot; id=&quot;lk-header-a&quot;&gt;Transaction A&lt;/div&gt;
            &lt;div class=&quot;timeline-body tx-a&quot; id=&quot;lk-timeline-a&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;timeline&quot;&gt;
            &lt;div class=&quot;timeline-header tx-b&quot; id=&quot;lk-header-b&quot;&gt;Transaction B&lt;/div&gt;
            &lt;div class=&quot;timeline-body tx-b&quot; id=&quot;lk-timeline-b&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;div class=&quot;lock-info&quot; id=&quot;lk-lock-info&quot;&gt;&lt;/div&gt;

    &lt;div class=&quot;db-section&quot;&gt;
        &lt;div class=&quot;db-label&quot; id=&quot;lk-db-label&quot;&gt;Row Versions on Disk&lt;/div&gt;
        &lt;div class=&quot;db-table-wrap&quot;&gt;
            &lt;table class=&quot;db-table&quot; id=&quot;lk-db-table&quot;&gt;&lt;/table&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;div class=&quot;explanation&quot; id=&quot;lk-explanation&quot;&gt;&lt;/div&gt;

    &lt;div class=&quot;nav&quot;&gt;
        &lt;button class=&quot;nav-btn&quot; id=&quot;lk-btn-back&quot; disabled=&quot;&quot;&gt;&amp;#9664; Back&lt;/button&gt;
        &lt;span class=&quot;step-indicator&quot; id=&quot;lk-step-indicator&quot;&gt;Step 1 of 8&lt;/span&gt;
        &lt;button class=&quot;nav-btn&quot; id=&quot;lk-btn-fwd&quot;&gt;Forward &amp;#9654;&lt;/button&gt;
    &lt;/div&gt;
    &lt;div class=&quot;key-hint&quot;&gt;Use &lt;kbd&gt;&amp;#8592;&lt;/kbd&gt; &lt;kbd&gt;&amp;#8594;&lt;/kbd&gt; arrow keys to navigate&lt;/div&gt;

&lt;/div&gt;

&lt;script&gt;
(function() {
    var TX_OLD = 99, TX_A = 100, TX_B = 200;

    function v(xmin, xminC, xmax, xmaxC, data, seenBy, dead, note, lock) {
        return { xmin: xmin, xminCommitted: xminC, xmax: xmax, xmaxCommitted: xmaxC, data: data, seenBy: seenBy || null, dead: dead || false, note: note || null, lock: lock || null };
    }

    var scenarios = [
        {
            id: 'for-share-nonrepeatable', name: 'FOR SHARE', tableName: 'accounts',
            txAId: TX_A, txBId: TX_B, columns: ['id', 'name', 'balance'],
            steps: [
                { txA: { sql: 'BEGIN;' }, txB: null, lockInfo: null, versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000]) ], explanation: 'Without locking, Read Committed allows non-repeatable reads: a second SELECT can return different data if another transaction commits in between. &lt;code&gt;FOR SHARE&lt;/code&gt; prevents this by blocking other transactions from modifying the locked rows.' },
                { txA: { sql: &quot;SELECT balance FROM accounts\n  WHERE name = 'Alice'\n  FOR SHARE;&quot;, result: '1000', resultType: 'ok' }, txB: null, lockInfo: '&lt;span class=&quot;info-label&quot;&gt;Row Locks&lt;/span&gt;&lt;span class=&quot;lock-held&quot;&gt;A: FOR SHARE on (Alice, 1000)&lt;/span&gt; \u2014 others can read, but cannot UPDATE or DELETE this row until A commits.', versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000], 'A', false, null, 'lock-a') ], explanation: 'A reads Alice\'s balance with &lt;code&gt;FOR SHARE&lt;/code&gt;. This acquires a &lt;strong&gt;row-level SHARE lock&lt;/strong&gt;. Other transactions can still read the row, but any attempt to UPDATE or DELETE will block until A releases the lock (at COMMIT or ROLLBACK).' },
                { txA: null, txB: { sql: 'BEGIN;' }, lockInfo: '&lt;span class=&quot;info-label&quot;&gt;Row Locks&lt;/span&gt;&lt;span class=&quot;lock-held&quot;&gt;A: FOR SHARE on (Alice, 1000)&lt;/span&gt;', versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000], null, false, null, 'lock-a') ], explanation: 'Transaction B begins.' },
                { txA: null, txB: { sql: &quot;UPDATE accounts SET balance = 500\n  WHERE name = 'Alice';&quot;, result: 'BLOCKED \u2014 waiting for A\'s lock', resultType: 'blocked' }, lockInfo: '&lt;span class=&quot;info-label&quot;&gt;Row Locks&lt;/span&gt;&lt;span class=&quot;lock-held&quot;&gt;A: FOR SHARE on (Alice, 1000)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;lock-waiting&quot;&gt;B: waiting for exclusive lock on Alice (blocked by A\'s SHARE lock)&lt;/span&gt;', versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000], null, false, null, 'lock-a') ], explanation: 'B tries to UPDATE Alice\'s row. The UPDATE needs an exclusive lock, but A holds a SHARE lock. &lt;strong&gt;B is blocked&lt;/strong&gt; \u2014 it must wait until A commits or rolls back. The row is unchanged on disk.' },
                { txA: { sql: &quot;SELECT balance FROM accounts\n  WHERE name = 'Alice';&quot;, result: '1000', resultType: 'ok' }, txB: null, lockInfo: '&lt;span class=&quot;info-label&quot;&gt;Row Locks&lt;/span&gt;&lt;span class=&quot;lock-held&quot;&gt;A: FOR SHARE on (Alice, 1000)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;lock-waiting&quot;&gt;B: still waiting&lt;/span&gt;', versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000], 'A', false, null, 'lock-a') ], explanation: 'A reads Alice\'s balance again \u2014 &lt;strong&gt;still 1000&lt;/strong&gt;. Because B is blocked by the lock, the row hasn\'t changed. This is a plain SELECT (no FOR SHARE needed for the re-read). &lt;span class=&quot;highlight&quot;&gt;Non-repeatable read prevented&lt;/span&gt; \u2014 the lock guarantees consistency within A\'s transaction.' },
                { txA: { sql: 'COMMIT;' }, txB: null, lockInfo: '&lt;span class=&quot;info-label&quot;&gt;Row Locks&lt;/span&gt;&lt;span class=&quot;lock-released&quot;&gt;A: lock released&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;lock-held&quot;&gt;B: unblocked \u2014 acquires exclusive lock&lt;/span&gt;', versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000]) ], explanation: 'A commits, releasing the SHARE lock. B is unblocked and its UPDATE can now proceed.' },
                { txA: null, txB: { sql: '(UPDATE proceeds)', result: 'UPDATE 1', resultType: 'ok' }, lockInfo: '&lt;span class=&quot;info-label&quot;&gt;Row Locks&lt;/span&gt;&lt;span class=&quot;lock-held&quot;&gt;B: exclusive lock on Alice&lt;/span&gt;', versions: [ v(TX_OLD, true, TX_B, false, [1, 'Alice', 1000]), v(TX_B, false, null, null, [1, 'Alice', 500], null, false, null, 'lock-b') ], explanation: 'B\'s UPDATE finally executes. A new version is created with balance = 500.' },
                { txA: null, txB: { sql: 'COMMIT;' }, lockInfo: '&lt;span class=&quot;info-label&quot;&gt;Row Locks&lt;/span&gt;&lt;span class=&quot;lock-released&quot;&gt;All locks released.&lt;/span&gt;', versions: [ v(TX_OLD, true, TX_B, true, [1, 'Alice', 1000], null, true), v(TX_B, true, null, null, [1, 'Alice', 500]) ], explanation: 'B commits. Final balance: 500. The key point: &lt;code&gt;FOR SHARE&lt;/code&gt; gave A a consistent view of the data throughout its transaction by preventing concurrent modifications, without needing to change the isolation level.' }
            ]
        },
        {
            id: 'for-update-lost-update', name: 'FOR UPDATE', tableName: 'accounts',
            txAId: TX_A, txBId: TX_B, columns: ['id', 'name', 'balance'],
            steps: [
                { txA: { sql: 'BEGIN;' }, txB: null, lockInfo: null, versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000]) ], explanation: 'Classic lost update scenario: two transactions both want to decrement Alice\'s balance by 100. Without locking, one update would overwrite the other. &lt;code&gt;FOR UPDATE&lt;/code&gt; prevents this.' },
                { txA: { sql: &quot;SELECT balance FROM accounts\n  WHERE name = 'Alice'\n  FOR UPDATE;&quot;, result: '1000', resultType: 'ok' }, txB: null, lockInfo: '&lt;span class=&quot;info-label&quot;&gt;Row Locks&lt;/span&gt;&lt;span class=&quot;lock-held&quot;&gt;A: FOR UPDATE on (Alice, 1000)&lt;/span&gt; \u2014 exclusive lock, no one else can read FOR UPDATE/SHARE or modify this row.', versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000], 'A', false, null, 'lock-a') ], explanation: 'A reads balance with &lt;code&gt;FOR UPDATE&lt;/code&gt;, acquiring an &lt;strong&gt;exclusive row lock&lt;/strong&gt;. This is stronger than FOR SHARE \u2014 it blocks both other writers AND other FOR UPDATE/FOR SHARE readers.' },
                { txA: null, txB: { sql: 'BEGIN;' }, lockInfo: '&lt;span class=&quot;info-label&quot;&gt;Row Locks&lt;/span&gt;&lt;span class=&quot;lock-held&quot;&gt;A: FOR UPDATE on (Alice, 1000)&lt;/span&gt;', versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000], null, false, null, 'lock-a') ], explanation: 'Transaction B begins. It also wants to decrement Alice\'s balance by 100.' },
                { txA: null, txB: { sql: &quot;SELECT balance FROM accounts\n  WHERE name = 'Alice'\n  FOR UPDATE;&quot;, result: 'BLOCKED \u2014 waiting for A\'s lock', resultType: 'blocked' }, lockInfo: '&lt;span class=&quot;info-label&quot;&gt;Row Locks&lt;/span&gt;&lt;span class=&quot;lock-held&quot;&gt;A: FOR UPDATE on (Alice, 1000)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;lock-waiting&quot;&gt;B: waiting for FOR UPDATE on Alice (blocked by A)&lt;/span&gt;', versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 1000], null, false, null, 'lock-a') ], explanation: 'B tries &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; on the same row. A already holds the lock. &lt;strong&gt;B is blocked.&lt;/strong&gt;' },
                { txA: { sql: &quot;UPDATE accounts\n  SET balance = 900\n  WHERE name = 'Alice';&quot;, result: 'UPDATE 1', resultType: 'ok' }, txB: null, lockInfo: '&lt;span class=&quot;info-label&quot;&gt;Row Locks&lt;/span&gt;&lt;span class=&quot;lock-held&quot;&gt;A: FOR UPDATE on Alice (updating)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;lock-waiting&quot;&gt;B: still waiting&lt;/span&gt;', versions: [ v(TX_OLD, true, TX_A, false, [1, 'Alice', 1000], null, false, null, null), v(TX_A, false, null, null, [1, 'Alice', 900], null, false, null, 'lock-a') ], explanation: 'A decrements: 1000 \u2212 100 = 900. A new version is created. B is still waiting.' },
                { txA: { sql: 'COMMIT;' }, txB: null, lockInfo: '&lt;span class=&quot;info-label&quot;&gt;Row Locks&lt;/span&gt;&lt;span class=&quot;lock-released&quot;&gt;A: lock released&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;lock-held&quot;&gt;B: unblocked \u2014 lock acquired, re-evaluates query&lt;/span&gt;', versions: [ v(TX_OLD, true, TX_A, true, [1, 'Alice', 1000], null, true), v(TX_A, true, null, null, [1, 'Alice', 900]) ], explanation: 'A commits. Lock released. B is unblocked. Crucially, in Read Committed, &lt;strong&gt;B\'s SELECT re-evaluates against the latest committed data&lt;/strong&gt; \u2014 it doesn\'t just use the old value.' },
                { txA: null, txB: { sql: '(SELECT resumes \u2014 re-evaluates)', result: '900', resultType: 'warn' }, lockInfo: '&lt;span class=&quot;info-label&quot;&gt;Row Locks&lt;/span&gt;&lt;span class=&quot;lock-held&quot;&gt;B: FOR UPDATE on (Alice, 900)&lt;/span&gt;', versions: [ v(TX_OLD, true, TX_A, true, [1, 'Alice', 1000], null, true), v(TX_A, true, null, null, [1, 'Alice', 900], 'B', false, null, 'lock-b') ], explanation: 'B\'s &lt;code&gt;SELECT FOR UPDATE&lt;/code&gt; completes. Because this is Read Committed, PostgreSQL &lt;strong&gt;re-checks the row&lt;/strong&gt; after unblocking: it sees the new committed version (balance = 900), not the old one. B now correctly knows the balance is 900.' },
                { txA: null, txB: { sql: &quot;UPDATE accounts\n  SET balance = 800\n  WHERE name = 'Alice';&quot;, result: 'UPDATE 1', resultType: 'ok' }, lockInfo: '&lt;span class=&quot;info-label&quot;&gt;Row Locks&lt;/span&gt;&lt;span class=&quot;lock-held&quot;&gt;B: FOR UPDATE on Alice (updating)&lt;/span&gt;', versions: [ v(TX_OLD, true, TX_A, true, [1, 'Alice', 1000], null, true), v(TX_A, true, TX_B, false, [1, 'Alice', 900]), v(TX_B, false, null, null, [1, 'Alice', 800], null, false, null, 'lock-b') ], explanation: 'B decrements: 900 \u2212 100 = 800. Both decrements are applied correctly.' },
                { txA: null, txB: { sql: 'COMMIT;' }, lockInfo: '&lt;span class=&quot;info-label&quot;&gt;Row Locks&lt;/span&gt;&lt;span class=&quot;lock-released&quot;&gt;All locks released.&lt;/span&gt;', versions: [ v(TX_OLD, true, TX_A, true, [1, 'Alice', 1000], null, true), v(TX_A, true, TX_B, true, [1, 'Alice', 900], null, true), v(TX_B, true, null, null, [1, 'Alice', 800]) ], explanation: '&lt;span class=&quot;highlight-ok&quot;&gt;Final balance: 800.&lt;/span&gt; Both decrements applied: 1000 \u2212 100 \u2212 100 = 800. No lost update. Three MVCC versions on disk show the full history. The key: &lt;code&gt;FOR UPDATE&lt;/code&gt; serialized the read-then-write pattern, and Read Committed\'s re-evaluation ensured B saw A\'s committed changes.' }
            ]
        },
        {
            id: 'for-update-write-skew', name: 'FOR UPDATE vs Write Skew', tableName: 'doctors',
            txAId: TX_A, txBId: TX_B, columns: ['id', 'name', 'on_call'],
            steps: [
                { txA: { sql: 'BEGIN;' }, txB: null, lockInfo: null, versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 'true']), v(TX_OLD, true, null, null, [2, 'Bob', 'true']) ], explanation: 'The doctors scenario: at least one must remain on call. Under plain Read Committed or Repeatable Read, both doctors can go off call (write skew). &lt;code&gt;FOR UPDATE&lt;/code&gt; can prevent this even in Read Committed.' },
                { txA: { sql: &quot;SELECT * FROM doctors\n  WHERE on_call = true\n  FOR UPDATE;&quot;, result: '2 rows', resultType: 'ok' }, txB: null, lockInfo: '&lt;span class=&quot;info-label&quot;&gt;Row Locks&lt;/span&gt;&lt;span class=&quot;lock-held&quot;&gt;A: FOR UPDATE on (Alice, true)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;lock-held&quot;&gt;A: FOR UPDATE on (Bob, true)&lt;/span&gt;&lt;br&gt;Both on-call rows are locked. No one can modify them.', versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 'true'], 'A', false, null, 'lock-a'), v(TX_OLD, true, null, null, [2, 'Bob', 'true'], 'A', false, null, 'lock-a') ], explanation: 'A reads all on-call doctors &lt;code&gt;FOR UPDATE&lt;/code&gt;, locking &lt;strong&gt;both rows&lt;/strong&gt;. This is the key difference \u2014 A locks the entire set it\'s making a decision about, not just the row it will modify.' },
                { txA: null, txB: { sql: 'BEGIN;' }, lockInfo: '&lt;span class=&quot;info-label&quot;&gt;Row Locks&lt;/span&gt;&lt;span class=&quot;lock-held&quot;&gt;A: FOR UPDATE on Alice, Bob&lt;/span&gt;', versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 'true'], null, false, null, 'lock-a'), v(TX_OLD, true, null, null, [2, 'Bob', 'true'], null, false, null, 'lock-a') ], explanation: 'Transaction B begins. Bob wants to go off call.' },
                { txA: null, txB: { sql: &quot;SELECT * FROM doctors\n  WHERE on_call = true\n  FOR UPDATE;&quot;, result: 'BLOCKED \u2014 waiting for A\'s lock', resultType: 'blocked' }, lockInfo: '&lt;span class=&quot;info-label&quot;&gt;Row Locks&lt;/span&gt;&lt;span class=&quot;lock-held&quot;&gt;A: FOR UPDATE on Alice, Bob&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;lock-waiting&quot;&gt;B: waiting for FOR UPDATE on on_call=true rows (blocked by A)&lt;/span&gt;', versions: [ v(TX_OLD, true, null, null, [1, 'Alice', 'true'], null, false, null, 'lock-a'), v(TX_OLD, true, null, null, [2, 'Bob', 'true'], null, false, null, 'lock-a') ], explanation: 'B also tries to &lt;code&gt;SELECT ... WHERE on_call = true FOR UPDATE&lt;/code&gt;. Both matching rows are locked by A. &lt;strong&gt;B is blocked.&lt;/strong&gt; B must wait until A releases the locks.' },
                { txA: { sql: &quot;UPDATE doctors SET on_call = false\n  WHERE name = 'Alice';&quot;, result: 'UPDATE 1', resultType: 'ok' }, txB: null, lockInfo: '&lt;span class=&quot;info-label&quot;&gt;Row Locks&lt;/span&gt;&lt;span class=&quot;lock-held&quot;&gt;A: FOR UPDATE on Alice (updating), Bob&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;lock-waiting&quot;&gt;B: still waiting&lt;/span&gt;', versions: [ v(TX_OLD, true, TX_A, false, [1, 'Alice', 'true'], null, false, null, null), v(TX_A, false, null, null, [1, 'Alice', 'false'], null, false, null, 'lock-a'), v(TX_OLD, true, null, null, [2, 'Bob', 'true'], null, false, null, 'lock-a') ], explanation: 'A checks the count (2), decides it\'s safe, and sets Alice off call. B is still blocked.' },
                { txA: { sql: 'COMMIT;' }, txB: null, lockInfo: '&lt;span class=&quot;info-label&quot;&gt;Row Locks&lt;/span&gt;&lt;span class=&quot;lock-released&quot;&gt;A: locks released&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;lock-held&quot;&gt;B: unblocked \u2014 re-evaluates WHERE on_call = true&lt;/span&gt;', versions: [ v(TX_OLD, true, TX_A, true, [1, 'Alice', 'true'], null, true), v(TX_A, true, null, null, [1, 'Alice', 'false']), v(TX_OLD, true, null, null, [2, 'Bob', 'true']) ], explanation: 'A commits. Alice is off call. Locks released. B is unblocked and &lt;strong&gt;re-evaluates the WHERE clause&lt;/strong&gt; against the latest committed data.' },
                { txA: null, txB: { sql: '(SELECT resumes \u2014 re-evaluates WHERE)', result: '1 row (Bob)', resultType: 'warn' }, lockInfo: '&lt;span class=&quot;info-label&quot;&gt;Row Locks&lt;/span&gt;&lt;span class=&quot;lock-held&quot;&gt;B: FOR UPDATE on (Bob, true)&lt;/span&gt;&lt;br&gt;Alice no longer matches &lt;code&gt;on_call = true&lt;/code&gt; \u2014 skipped.', versions: [ v(TX_OLD, true, TX_A, true, [1, 'Alice', 'true'], null, true), v(TX_A, true, null, null, [1, 'Alice', 'false']), v(TX_OLD, true, null, null, [2, 'Bob', 'true'], 'B', false, null, 'lock-b') ], explanation: 'B\'s query re-evaluates. Alice now has &lt;code&gt;on_call = false&lt;/code&gt; \u2014 she no longer matches the WHERE clause and is &lt;strong&gt;skipped&lt;/strong&gt;. B only gets Bob. &lt;strong&gt;Count = 1.&lt;/strong&gt; B knows it\'s not safe to go off call!' },
                { txA: null, txB: { sql: 'COMMIT;\n-- (no update made)', result: 'Bob stays on call', resultType: 'ok' }, lockInfo: '&lt;span class=&quot;info-label&quot;&gt;Row Locks&lt;/span&gt;&lt;span class=&quot;lock-released&quot;&gt;All locks released.&lt;/span&gt;', versions: [ v(TX_OLD, true, TX_A, true, [1, 'Alice', 'true'], null, true), v(TX_A, true, null, null, [1, 'Alice', 'false']), v(TX_OLD, true, null, null, [2, 'Bob', 'true']) ], explanation: '&lt;span class=&quot;highlight-ok&quot;&gt;Write skew prevented!&lt;/span&gt; B saw only 1 doctor on call and chose not to go off call. Final state: Alice off, Bob on. The constraint is preserved. The combination of &lt;code&gt;FOR UPDATE&lt;/code&gt; (which locks the decision set) and Read Committed\'s &lt;strong&gt;re-evaluation after unblocking&lt;/strong&gt; gives us write skew prevention without needing Serializable isolation.' }
            ]
        }
    ];

    var state = { scenarioIdx: 0, step: 0 };
    function cur() { return scenarios[state.scenarioIdx]; }
    function txidClass(t) { return t === TX_A ? 'tx-a-color' : t === TX_B ? 'tx-b-color' : 'tx-old'; }

    function renderTabs() {
        var c = document.getElementById('lk-tabs'); c.innerHTML = '';
        scenarios.forEach(function(s, i) {
            var b = document.createElement('button');
            b.className = 'tab' + (i === state.scenarioIdx ? ' active' : '');
            b.textContent = s.name;
            b.addEventListener('click', function() { state.scenarioIdx = i; state.step = 0; render(); });
            c.appendChild(b);
        });
    }

    function renderHeaders() {
        var s = cur();
        document.getElementById('lk-header-a').innerHTML = 'Transaction A &lt;span class=&quot;txid&quot;&gt;txid ' + s.txAId + '&lt;/span&gt;';
        document.getElementById('lk-header-b').innerHTML = 'Transaction B &lt;span class=&quot;txid&quot;&gt;txid ' + s.txBId + '&lt;/span&gt;';
    }

    function renderTimelines() {
        var s = cur();
        var aC = document.getElementById('lk-timeline-a'); aC.innerHTML = '';
        var bC = document.getElementById('lk-timeline-b'); bC.innerHTML = '';
        s.steps.forEach(function(sd, idx) {
            function mkCard(tx, side) {
                var card = document.createElement('div');
                if (tx) {
                    card.className = 'step-card';
                    if (idx &lt; state.step) card.className += ' past';
                    else if (idx === state.step) card.className += ' current-' + side;
                    else card.className += ' future';
                    var sql = document.createElement('div'); sql.className = 'sql'; sql.textContent = tx.sql; card.appendChild(sql);
                    if (tx.result &amp;&amp; idx &lt;= state.step) {
                        var r = document.createElement('div'); r.className = 'result result-' + tx.resultType; r.textContent = '\u2192 ' + tx.result; card.appendChild(r);
                    }
                } else {
                    card.className = idx &lt;= state.step ? 'step-card idle' : 'step-card future';
                    if (idx &lt;= state.step) card.textContent = '\u2014';
                }
                return card;
            }
            aC.appendChild(mkCard(sd.txA, 'a'));
            bC.appendChild(mkCard(sd.txB, 'b'));
        });
    }

    function renderLockInfo() {
        var sd = cur().steps[state.step];
        var el = document.getElementById('lk-lock-info');
        if (sd.lockInfo) { el.innerHTML = sd.lockInfo; el.className = 'lock-info visible'; }
        else { el.className = 'lock-info'; el.innerHTML = ''; }
    }

    function renderTable() {
        var s = cur(), sd = s.steps[state.step];
        document.getElementById('lk-db-label').innerHTML = s.tableName + ' &lt;span&gt;\u2014 MVCC row versions on disk&lt;/span&gt;';
        var table = document.getElementById('lk-db-table'); table.innerHTML = '';
        var thead = document.createElement('thead'), hr = document.createElement('tr');
        ['xmin','xmax'].forEach(function(c){ var th=document.createElement('th'); th.className='mvcc-col'; th.textContent=c; hr.appendChild(th); });
        s.columns.forEach(function(c){ var th=document.createElement('th'); th.textContent=c; hr.appendChild(th); });
        var thV=document.createElement('th'); thV.className='mvcc-col'; thV.textContent=''; hr.appendChild(thV);
        thead.appendChild(hr); table.appendChild(thead);
        var tbody = document.createElement('tbody');
        sd.versions.forEach(function(ver) {
            var tr = document.createElement('tr'), cls = [];
            if (ver.dead) cls.push('version-dead');
            if (ver.seenBy === 'A') cls.push('version-seen-a');
            if (ver.seenBy === 'B') cls.push('version-seen-b');
            if (!ver.xminCommitted) cls.push('version-uncommitted');
            tr.className = cls.join(' ');
            var tdm = document.createElement('td'); tdm.className = 'mvcc-cell';
            var chip = document.createElement('span'); chip.className = 'txid-chip ' + txidClass(ver.xmin); chip.textContent = ver.xmin; tdm.appendChild(chip);
            if (ver.xminCommitted) { var m=document.createElement('span'); m.className='committed-mark'; m.textContent=' \u2714'; tdm.appendChild(m); }
            else { var m=document.createElement('span'); m.className='uncommitted-mark'; m.textContent=' pending'; tdm.appendChild(m); }
            tr.appendChild(tdm);
            var tdx = document.createElement('td'); tdx.className = 'mvcc-cell';
            if (ver.xmax !== null) {
                var cx=document.createElement('span'); cx.className='txid-chip '+txidClass(ver.xmax); cx.textContent=ver.xmax; tdx.appendChild(cx);
                if (ver.xmaxCommitted) { var mx=document.createElement('span'); mx.className='committed-mark'; mx.textContent=' \u2714'; tdx.appendChild(mx); }
                else { var mx=document.createElement('span'); mx.className='uncommitted-mark'; mx.textContent=' pending'; tdx.appendChild(mx); }
            } else { var em=document.createElement('span'); em.className='xmax-empty'; em.textContent='\u2014'; tdx.appendChild(em); }
            tr.appendChild(tdx);
            ver.data.forEach(function(val){ var td=document.createElement('td'); td.textContent=val; tr.appendChild(td); });
            var tdv = document.createElement('td'); tdv.className = 'mvcc-cell';
            if (ver.seenBy) { var badge=document.createElement('span'); badge.className='seen-badge seen-badge-'+ver.seenBy.toLowerCase(); badge.textContent='\u2190 '+ver.seenBy+' reads'; tdv.appendChild(badge); }
            if (ver.lock) {
                var lb=document.createElement('span');
                if (ver.lock === 'lock-a') { lb.className='lock-badge'; lb.textContent='\uD83D\uDD12 A'; }
                else if (ver.lock === 'lock-b') { lb.className='lock-badge'; lb.textContent='\uD83D\uDD12 B'; }
                else if (ver.lock === 'waiting-b') { lb.className='lock-badge lock-badge-waiting'; lb.textContent='\u23f3 B'; }
                tdv.appendChild(lb);
            }
            if (ver.note) { var noteEl=document.createElement('span'); noteEl.className='version-note'; noteEl.textContent=ver.note; tdv.appendChild(noteEl); }
            tr.appendChild(tdv); tbody.appendChild(tr);
        });
        table.appendChild(tbody);
    }

    function renderNav() {
        var total = cur().steps.length;
        document.getElementById('lk-btn-back').disabled = state.step === 0;
        document.getElementById('lk-btn-fwd').disabled = state.step === total - 1;
        document.getElementById('lk-step-indicator').textContent = 'Step ' + (state.step + 1) + ' of ' + total;
    }

    function render() {
        renderTabs(); renderHeaders(); renderTimelines(); renderLockInfo(); renderTable();
        document.getElementById('lk-explanation').innerHTML = cur().steps[state.step].explanation;
        renderNav();
    }

    document.getElementById('lk-btn-back').addEventListener('click', function() { if (state.step &gt; 0) { state.step--; render(); } });
    document.getElementById('lk-btn-fwd').addEventListener('click', function() { if (state.step &lt; cur().steps.length - 1) { state.step++; render(); } });

    var demo = document.getElementById('lk-demo');
    demo.addEventListener('keydown', function(e) {
        if (e.key === 'ArrowLeft' &amp;&amp; state.step &gt; 0) { state.step--; render(); }
        if (e.key === 'ArrowRight' &amp;&amp; state.step &lt; cur().steps.length - 1) { state.step++; render(); }
    });
    demo.setAttribute('tabindex', '0');

    render();
})();
&lt;/script&gt;

&lt;h1 id=&quot;thanks-and-further-reading&quot;&gt;Thanks and further reading&lt;/h1&gt;

&lt;p&gt;The inspiration for building an interactive simulator came from the &lt;a href=&quot;https://github.com/TheOtherBrian1&quot;&gt;TheOtherBrian1&lt;/a&gt; excellent site &lt;a href=&quot;https://postgreslocksexplained.com/&quot;&gt;“Postgres Locks Explained”&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;PostgreSQL documentation on isolation levels and locking:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/transaction-iso.html&quot;&gt;https://www.postgresql.org/docs/current/transaction-iso.html&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/explicit-locking.html&quot;&gt;https://www.postgresql.org/docs/current/explicit-locking.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI tools (Gemini, ChatGPT) were used to improve grammar and clarity of the text. The interactive simulators were built with the help of Claude Code.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Using Generative AI tooling with Clojure</title>
      <link>http://dev.solita.fi/2026/02/11/using-ai-tooling-with-clojure.html</link>
      <pubDate>Wed, 11 Feb 2026 00:00:00 +0000</pubDate>
      <author>open@solita.fi (Solita Oy)</author>
      <guid>http://dev.solita.fi/2026/02/11/using-ai-tooling-with-clojure</guid>
	  
      <description>&lt;h2 id=&quot;clojure-is-easy-to-read-for-humans-and-ais&quot;&gt;Clojure is easy to read for humans and AIs&lt;/h2&gt;

&lt;p&gt;Code written in Clojure is expressive and concise, but is still easy
to reason about. In his &lt;a href=&quot;https://dl.acm.org/doi/pdf/10.1145/3386321&quot;&gt;“History of Clojure”&lt;/a&gt; paper Rich
Hickey, the original author of Clojure states his motivations for
building a new programming language:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Most developers are primarily engaged in making
systems that acquire, extract, transform, maintain, analyze, transmit and render information—facts
about the world … As programs grew large, they required
increasingly Herculean efforts to change while maintaining all of the presumptions around state
and relationships, never mind dealing with race conditions as concurrency was increasingly in play.
And we faced encroaching, and eventually crippling, coupling, and huge codebases, due directly to
specificity (best-practice encapsulation, abstraction, and parameterization notwithstanding). C++
builds of over an hour were common.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As a result, Clojure programs are very much focused on dealing with
data and do it safely in concurrent programs. We have by default
immutable data structures, easy to use literal representations of the
most common collection types (lists, vectors, sets and maps) and a very
regular syntax. A typical Clojure program has way less ceremony and
boilerplate, not to mention weird quirks to deal with compared to many
more programming languages such as Java, C#, Typescript or Python.&lt;/p&gt;

&lt;p&gt;This means that large language models have less to deal with when
reading or writing Clojure code. We have some evidence in Martin
Alderson’s article that Clojure is &lt;a href=&quot;https://martinalderson.com/posts/which-programming-languages-are-most-token-efficient/&quot;&gt;token efficient&lt;/a&gt; compared to most other popular programming languages.&lt;/p&gt;

&lt;p&gt;When we author code with generative AI tools, a developer reviewing it has less
code to read in a format that easy to reason about.&lt;/p&gt;

&lt;h2 id=&quot;clojure-mcp-boosts-agentic-development-workflows&quot;&gt;Clojure MCP boosts Agentic development workflows&lt;/h2&gt;

&lt;p&gt;The REPL driven workflow speeds up the feedback cycle in normal
development modes. &lt;strong&gt;R&lt;/strong&gt;ead &lt;strong&gt;E&lt;/strong&gt;val &lt;strong&gt;P&lt;/strong&gt;rint &lt;strong&gt;L&lt;/strong&gt;oop is a concept in many programming languages in the LISP family such as Common Lisp, Scheme and Clojure. It allows the developer to tap into and evaluate code in a running instance of the application they are developing. With good editor integration, this allows smooth and frictionless testing of the code under development in an interactive workflow.&lt;/p&gt;

&lt;p&gt;With the addition of &lt;a href=&quot;https://modelcontextprotocol.io/introduction&quot;&gt;MCP (Model Context Protocol)&lt;/a&gt;
agents have gained access to a lot of tooling. In May 2025 Bruce
Hauman announced his &lt;a href=&quot;https://github.com/bhauman/clojure-mcp&quot;&gt;Clojure MCP&lt;/a&gt;
that provides agents access to the REPL. Now
AI agents such as Claude Code, Copilot CLI and others can reach inside
the application as it is being developed, try code changes live, look
at the internal state of the application and benefit from all of the
interactivity that human developers have when working with the REPL.&lt;/p&gt;

&lt;p&gt;It also provides efficient structural editing capabilities to the
agents, making them less error-prone when editing Clojure source
code. Because Clojure code is written as Clojure data structures,
programmatic edits to the source code are a breeze.&lt;/p&gt;

&lt;p&gt;We can even hot load dependencies to a running application without
losing the application state! This is a SKILL.md file I have added to
my project to guide agents in harnessing this power:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;---
name: adding-clojure-dependencies
description: Adds clojure dependencies to the project. Use this when asked to add a dependency to the project
---

To add dependencies to deps.edn do the following:

1. Find the dependency in maven central repo or clojars
2. Identify the latest release version (no RCs unless specified)
3. Add dependency to either main list (for clojure dependencies),
   :test alias (for test dependencies) or :dev alias (for development dependencies). Use the REPL and
   `rewrite-edn` to edit the file
4. Reload dependencies in the REPL using `(clojure.repl.deps/sync-deps)`
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In my experience, with the Clojure MCP coding agents have a far easier
time troubleshooting and debugging compared to having them just
analyze source code, logs and stacktraces.&lt;/p&gt;

&lt;p&gt;As a developer, we can also connect to the same REPL as the coding
agent, making it easy to step in and aid the agent when it gets
stuck. In my workflows, I might look at the code the agent produced
and test it in the REPL as well, make changes as required and instruct
the agent to read what I did. This gives another collaborative
dimension to standard prompting techniques that are normally
associated with generative AI development.&lt;/p&gt;

&lt;h2 id=&quot;getting-ai-to-speak-clojure&quot;&gt;Getting AI to speak Clojure&lt;/h2&gt;

&lt;p&gt;Generating and analyzing code with AI tooling is just one way
to apply AI in software development. As developers, we should
understand the potential for embedding AI functionality at the
application level too. LLMs seem to be good at understanding my
intentions, even if they don’t necessarily produce the right
results. One possibility is to take input provided by a human and
enrich it with data so that further processing becomes easier. For the
sake of experiment, let’s look at a traditional flow of making a
support request.&lt;/p&gt;

&lt;p&gt;The user goes to the portal and their first task is to identify the
correct topic under which this support request belongs to. They
usually have to classify the severity of the issue as well. Then they
describe what problem they have and add their contact
information. With this flow, there’s a large chance that the user
misclassified their support request, causing delays in getting the
work in front of the right person, making the user experience poor and
causing frustrations in the people handling the support requests. From
the user’s point of view they don’t care about which department
picks up the request, so this system is pushing the support
organization’s concerns to the end user.&lt;/p&gt;

&lt;p&gt;What if we could avoid all that and have the request routed
automatically to the right backlog? Enter OpenAI’s &lt;a href=&quot;https://developers.openai.com/api/reference/resources/responses/methods/create&quot;&gt;Requests API&lt;/a&gt;
which can handle text, image and various file inputs to generate text
or JSON outputs. The &lt;em&gt;json_schema&lt;/em&gt; response format is interesting in
particular because we can express the desired result format in a
manner that we can then use to process the response programmatically
down the line.&lt;/p&gt;

&lt;p&gt;In the Clojure world, we often use &lt;a href=&quot;https://github.com/metosin/malli&quot;&gt;Malli&lt;/a&gt; to define our data models. We
can use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;malli.json-schema&lt;/code&gt; to transform our malli schemas into
JSON schema that the endpoint understands, and then use
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;malli.transform&lt;/code&gt; to translate the response from JSON back to Clojure
data.&lt;/p&gt;

&lt;p&gt;A &lt;a href=&quot;https://gist.github.com/ikitommi/e643713719c3620f943ef34086451c69&quot;&gt;similar idea&lt;/a&gt;
has been shown previously by Tommi Reiman, author of Malli.&lt;/p&gt;

&lt;p&gt;Note that the choice of model can have a big effect on your output!&lt;/p&gt;

&lt;div class=&quot;language-clojure highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;malli.json-schema&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:as&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mjs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;malli.core&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:as&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;malli.transform&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:as&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cheshire.core&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:as&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;org.httpkit.client&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:as&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;defn&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;structured-output&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;api-endpoint-url&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;api-key&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;malli-schema&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;;; Convert Malli schema to JSON Schema&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;json-schema&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mjs/transform&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;malli-schema&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

        &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;;; Build request body&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:model&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;gpt-4o&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;;; consult your service provider for available models&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:input&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:text&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:format&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:type&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;json_schema&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                              &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;response&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                              &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:strict&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                              &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:schema&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;json-schema&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}}}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

        &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;;; Make HTTP request&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;http/post&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                   &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;api-endpoint-url&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/v1/responses&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                   &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:headers&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Authorization&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Bearer &quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;api-key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                              &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;application/json&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:body&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;json/generate-string&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)})&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

        &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;;; Parse response&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parsed-response&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;json/parse-string&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:body&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

        &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;;; Extract structured data from response&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parsed-response&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:output&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:content&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parsed-data&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;json/parse-string&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

        &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;;; Decode using Malli transformer&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;m/decode&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;malli-schema&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parsed-data&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mt/json-transformer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

    &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;;; Validate and return&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;when-not&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;m/validate&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;malli-schema&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;throw&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ex-info&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Response does not match schema&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:schema&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;malli-schema&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                       &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:result&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;})))&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;structured-output&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
   &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get-base-url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
   &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get-api-key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
   &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:map&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:department&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:enum&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:licences&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:hardware&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:accounts&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:maintenance&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:other&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:severity&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:enum&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:low&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:medium&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:critical&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:request&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:contact-information&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:map&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                           &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:optional&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                           &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:phone&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:optional&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                           &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:raw&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]]]]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
   &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Hi, my laptop diesn't let me login anymore. Can't work. What do? t. Hank PS call me 555-1343 because I can't access email&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;; =&amp;gt; {:department :hardware,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;;     :severity :critical,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;;     :request &quot;Laptop doesn't let me login anymore. Can't work.&quot;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;;     :contact-information&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;;     {:raw &quot;Hank, phone: 555-1343, cannot access email&quot;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;;      :phone &quot;555-1343&quot;}}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Notice how the model was able to process the human input that contained typos and informal, broken language.
One doesn’t usually have to use any of the more powerful and expensive models to do this level of work. The older &lt;em&gt;json_object&lt;/em&gt; response format has some capabilities and supports even lighter models. See &lt;a href=&quot;https://developers.openai.com/api/docs/guides/structured-outputs&quot;&gt;OpenAI’s documentation&lt;/a&gt; for reference.&lt;/p&gt;

&lt;p&gt;I think methods like this make it easy to embed LLM-enabled functionality in Clojure applications, giving them capabilities that are normally very hard to implement using traditional methods.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DISCLAIMER&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you were to implement the above example in your application as is, you’d be sending personal data to OpenAI. As developers, we should always consider the larger implications. You can deploy models locally in your region and keep data residency and processing within the EU region for example by using &lt;a href=&quot;https://www.solita.fi/news/solita-brings-functionai-to-upcloud-enabling-a-full-eu-sovereign-ai-platform/&quot;&gt;Solita’s FunctionAI in UpCloud&lt;/a&gt; or other service providers.&lt;/p&gt;

&lt;h2 id=&quot;further-reading&quot;&gt;Further reading&lt;/h2&gt;

&lt;p&gt;If you’re new to agentic development, &lt;a href=&quot;https://dev.solita.fi/2026/02/10/prompt-engineering-101.html&quot;&gt;Prompt engineering 101&lt;/a&gt; is a great starter for how to get past the first hurdles.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Prompt Engineering 101</title>
      <link>http://dev.solita.fi/2026/02/10/prompt-engineering-101.html</link>
      <pubDate>Tue, 10 Feb 2026 00:00:00 +0000</pubDate>
      <author>open@solita.fi (Solita Oy)</author>
      <guid>http://dev.solita.fi/2026/02/10/prompt-engineering-101</guid>
	  
      <description>&lt;p&gt;Let’s be honest. Those who have embraced AI as part of their daily development work have noticed significant improvements in both speed and &lt;em&gt;quality&lt;/em&gt; (yes, we are &lt;em&gt;not&lt;/em&gt; talking about vibe coding). So the question is no longer “Is AI useful for coding?” but rather “How do I get the most out of it?”&lt;/p&gt;

&lt;p&gt;Why do some developers see tremendous benefits while others end up with spaghetti code and hallucinations? I took on a challenge at the end of last year to only work by prompting, in order to learn the ins and outs of AI-assisted development.&lt;/p&gt;

&lt;p&gt;In this post, I’ll share the key lessons from that journey, and hopefully inspire you to give it (another) try.&lt;/p&gt;

&lt;h2 id=&quot;so-whats-the-problem&quot;&gt;So What’s the Problem?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bad prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;My app hangs when users log in but only sometimes and I’ve tried everything, can you fix it?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A common mistake when starting out is selecting the wrong problem for AI to solve. I made this mistake myself and have observed many others doing the same. The workflow typically goes something like this:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;There’s an issue to fix.&lt;/li&gt;
  &lt;li&gt;You try the obvious solution (and it doesn’t work).&lt;/li&gt;
  &lt;li&gt;You go deeper, read more code, debug, and exhaust all your own resources.&lt;/li&gt;
  &lt;li&gt;You finally ask the AI for help.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You &lt;em&gt;might&lt;/em&gt; get a useful hint, but more often than not, the results miss the mark entirely. So you dismiss the AI and go back to debugging by hand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;I’m debugging a login issue where the app sometimes hangs.&lt;/p&gt;

  &lt;p&gt;Look at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Services/AuthService.cs&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Controllers/AuthController.cs&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Middleware/JwtMiddleware.cs&lt;/code&gt; to understand the login flow.&lt;/p&gt;

  &lt;p&gt;Look at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Repositories/UserRepository.cs&lt;/code&gt; to see how we fetch the user from db.&lt;/p&gt;

  &lt;p&gt;Here is our logic in the cache layer: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Services/TokenCacheService.cs&lt;/code&gt;&lt;/p&gt;

  &lt;p&gt;Analyze the flow and give me suggestions where the issue might be.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When I set the prompting challenge for myself, I quickly realized that using AI effectively requires a mental shift away from thinking of it as an “all-knowing entity” or a sparring partner. Instead, &lt;em&gt;you need to guide the AI like you would instruct a junior developer&lt;/em&gt;. Once you are skilled enough in prompting, you can treat it more like a peer at the same level as you. Once I started giving the agent simple and clear tasks, I found it performed remarkably well!&lt;/p&gt;

&lt;p&gt;Here’s the thing: &lt;strong&gt;if you don’t know how something should be done, the AI doesn’t know either.&lt;/strong&gt; Or rather, if you don’t know what you want from the AI, how can you expect it to deliver?&lt;/p&gt;

&lt;p&gt;AI is fundamentally a guessing machine. Without clear guidance, it will confidently guess and keep guessing. The quality of your output is directly tied to the clarity of your instructions.&lt;/p&gt;

&lt;h2 id=&quot;context-is-everything&quot;&gt;Context Is Everything&lt;/h2&gt;

&lt;p&gt;The key to effective prompting is understanding how AI context works. While the model is trained on vast amounts of data from across the internet, the context provided in your current chat session carries significantly more weight. I initially assumed that since JavaScript dominates AI training data, the model would perform poorly with other languages. This assumption was incorrect. Once you grasp how context influences output, you can achieve excellent results regardless of programming language or tech stack.&lt;/p&gt;

&lt;p&gt;Here is another example:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bad prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;Create a new API endpoint for user profiles.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This prompt would likely result in unexpected changes across your codebase and generic code based on common conventions from training data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;Create a new API endpoint for fetching user profiles.&lt;/p&gt;

  &lt;p&gt;Look at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Controllers/ProductController.cs&lt;/code&gt; for reference on how we structure our endpoints and routing attributes.&lt;/p&gt;

  &lt;p&gt;Look at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ClientApp/src/views/UserProfile.tsx&lt;/code&gt; and see what placeholders we are using to deduce what fields should be returned.&lt;/p&gt;

  &lt;p&gt;Follow the same patterns for error handling and response formatting.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second prompt is more effective because it provides the AI with concrete reference points from your actual codebase. Instead of leaving the AI to guess your project’s conventions, you’re explicitly pointing it to existing implementations. The AI can now read those files, understand your patterns for routing, controller structure, and error handling, and produce code that fits seamlessly into your project.&lt;/p&gt;

&lt;p&gt;But what if I told you that even the bad prompt can yield excellent results? Enter Planning mode.&lt;/p&gt;

&lt;h2 id=&quot;just-use-planning-mode&quot;&gt;Just Use Planning Mode&lt;/h2&gt;

&lt;p&gt;I’m a huge advocate of Copilot’s Planning mode. In Agent mode, Copilot eagerly starts executing, often in the wrong direction.&lt;/p&gt;

&lt;p&gt;Planning mode, on the other hand, forces the AI to stop, think, and &lt;em&gt;explore the codebase&lt;/em&gt; first. It creates a step-by-step plan of what it &lt;em&gt;intends&lt;/em&gt; to do and asks for clarifying questions. This simple change makes a massive difference. You can catch misunderstandings early, redirect the approach, or refine your requirements before any code is written. And most importantly, it automatically loads exactly the files and references it needs into context so it can complete the task without going off the rails.&lt;/p&gt;

&lt;p&gt;Let’s see how the same “bad prompt” can outperform our carefully crafted “better prompt” when used with Planning mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;Create a new API endpoint for user profiles.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Planning Agent:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;I’ll help you create a new API endpoint for user profiles. Let me first understand your project structure and conventions.&lt;/p&gt;

  &lt;p&gt;&lt;strong&gt;Gathering context…&lt;/strong&gt;&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;Reading &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Controllers/&lt;/code&gt; to understand your controller patterns and routing attributes&lt;/li&gt;
    &lt;li&gt;Reading &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Models/User.cs&lt;/code&gt; to understand the User entity&lt;/li&gt;
    &lt;li&gt;Reading &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Services/&lt;/code&gt; to see how business logic is structured&lt;/li&gt;
    &lt;li&gt;Reading &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ClientApp/src/views/UserProfile.tsx&lt;/code&gt; to see what fields the frontend expects&lt;/li&gt;
  &lt;/ul&gt;

  &lt;p&gt;&lt;strong&gt;⚠️ I noticed something:&lt;/strong&gt;
Some of your GET endpoints use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IDistributedCache&lt;/code&gt; with a 5-minute TTL. Since user profiles are read frequently but rarely updated, I’ll apply the same caching strategy here.&lt;/p&gt;

  &lt;p&gt;&lt;strong&gt;Here’s my plan:&lt;/strong&gt;&lt;/p&gt;
  &lt;ol&gt;
    &lt;li&gt;Create &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Controllers/UserController.cs&lt;/code&gt; following the pattern in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductController.cs&lt;/code&gt;&lt;/li&gt;
    &lt;li&gt;Add route &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api/users/{id}&lt;/code&gt; using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[Route]&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[HttpGet]&lt;/code&gt; attributes&lt;/li&gt;
    &lt;li&gt;Create &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DTOs/UserProfileDto.cs&lt;/code&gt; with fields: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Id&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Name&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Email&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Avatar&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CreatedAt&lt;/code&gt; (based on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UserProfile.tsx&lt;/code&gt; usage)&lt;/li&gt;
    &lt;li&gt;Implement Redis caching with 5-minute TTL using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IDistributedCache&lt;/code&gt;, same as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductController.cs&lt;/code&gt;&lt;/li&gt;
    &lt;li&gt;Use the same &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ApiResponse&amp;lt;T&amp;gt;&lt;/code&gt; wrapper and error handling as other controllers&lt;/li&gt;
    &lt;li&gt;Add input validation using FluentValidation&lt;/li&gt;
  &lt;/ol&gt;

  &lt;p&gt;&lt;strong&gt;Questions before I proceed:&lt;/strong&gt;&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;Should this endpoint require authentication?&lt;/li&gt;
    &lt;li&gt;Can users view other users’ profiles? If so, should we use a separate DTO that omits sensitive fields like email?&lt;/li&gt;
  &lt;/ul&gt;

  &lt;p&gt;Does this plan look correct?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now we are cooking! You can see exactly what the agent intends to do before it writes a single line of code. You can answer its questions, refine the plan, and polish it until it’s exactly right. In my experience, the agent often catches details I overlooked (like the caching pattern example above), which would have caused inconsistencies later.&lt;/p&gt;

&lt;p&gt;Here’s the magic of context. Once the agent has explored your codebase and built a plan, you don’t need to start from scratch for related tasks. The relevant files are already loaded, and the agent remembers what it just did. Your next prompts can be much simpler:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;Update the UI to call the new endpoint and replace the placeholders with real data.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;You:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;Add unit tests for the new endpoint. Look at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductControllerTests.cs&lt;/code&gt; for reference.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Notice that we still point the agent to the right reference file when needed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;Actually the CreatedAt timestamp is not needed. Remove it from the response dto and from the UI.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;You:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;When the user id does not exist we hit 404 but in this case we want to redirect to the front page. Look at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductPage.tsx&lt;/code&gt; for example.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A word of warning about &lt;a href=&quot;https://research.trychroma.com/context-rot&quot;&gt;context rot&lt;/a&gt;. AI memory behaves a lot like human memory: it retains what was discussed at the beginning and end of a conversation, but the middle gets hazy. If you keep going in the same chat session for too long, the agent becomes overwhelmed with too much information and starts getting confused. A good habit is to start a fresh chat session for each new feature, keeping the context focused on the task at hand.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“What about the hard stuff like race conditions, complex state machines, and security edge cases?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Well… you’re not there yet. Start by automating the easy tasks you already know how to solve. AI performs best when you can clearly describe the outcome. If you know you need to extract logic into a service and refactor 10 files to use it, let the AI do that and save time. You probably would’ve made a copy-paste error anyway, or left a misleading comment in.&lt;/p&gt;

&lt;p&gt;But once you’ve built that foundation, the complex problems are exactly where good prompting shines. The AI struggles when you’re vague, but if you can enumerate the edge cases, describe the state transitions, or specify the security requirements, it handles them remarkably well. Complexity isn’t the enemy. &lt;em&gt;Unclear&lt;/em&gt; complexity is. AI can solve any complex task, but the challenge is breaking it down into clear, actionable steps. Ask yourself: how would you delegate this task to a junior developer?&lt;/p&gt;

&lt;h2 id=&quot;prompting-is-a-skill&quot;&gt;Prompting Is a Skill&lt;/h2&gt;

&lt;p&gt;Once you get the hang of it, this is what “coding” looks like for me now. It’s going back and forth with the AI to refine the plan until it’s right. I get to focus on the big picture and how the pieces fit together. In the end, I design better features, improve the codebase through refactoring, and save time because the code writing is automated.&lt;/p&gt;

&lt;p&gt;But getting here wasn’t instant. At first, I felt like an idiot when nothing worked. After my initial attempts, I caught myself thinking “I can code faster by hand than fixing the AI’s mistakes.” It took about two weeks to break even with manual coding, and another few weeks before the new approach finally clicked.&lt;/p&gt;

&lt;p&gt;Prompting is a skill just like any other. You have to accept a small ego hit and feel dumb for a bit to make progress. The hardest part is getting started &lt;strong&gt;and keeping going.&lt;/strong&gt; You don’t yet know how to talk to the agent. Your prompts will fail. You’ll redo things. A lot. But with each mistake, you learn what works and what doesn’t.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The point comes eventually when you realize you’ve done a day’s worth of work in minutes without the AI making a single mistake.&lt;/strong&gt; After that, there’s no going back.&lt;/p&gt;

&lt;h2 id=&quot;how-not-to-get-overwhelmed&quot;&gt;How Not to Get Overwhelmed&lt;/h2&gt;

&lt;p&gt;The world of agentic coding is evolving way too fast for anyone to stay on top of everything. New concepts emerge constantly: &lt;a href=&quot;https://modelcontextprotocol.io/introduction&quot;&gt;MCP (Model Context Protocol)&lt;/a&gt; lets agents connect to databases, APIs, and external tools. &lt;a href=&quot;https://docs.github.com/en/copilot/concepts/agents/about-agent-skills&quot;&gt;Agent Skills&lt;/a&gt; give Copilot specialized capabilities for specific tasks. Multi-agent orchestrators like &lt;a href=&quot;https://github.com/steveyegge/gastown&quot;&gt;Gas Town&lt;/a&gt; let you coordinate 20-30 Claude Code agents working in parallel with persistent work tracking. And &lt;a href=&quot;https://code.visualstudio.com/docs/copilot/customization/custom-agents&quot;&gt;custom agents&lt;/a&gt; let you create specialized assistants tailored to your workflow.&lt;/p&gt;

&lt;p&gt;It can feel overwhelming. If I changed my workflow every time a new tool came up, I wouldn’t get any work done. And it all boils down to context management: these features are just different ways to feed better instructions to the model. That said, model choice does matter. Models have improved dramatically, and in my opinion, &lt;strong&gt;Claude Opus 4.5&lt;/strong&gt; is currently the best for coding.&lt;/p&gt;

&lt;p&gt;My advice is to tune out the noise. First focus on mastering the fundamentals: understanding context, writing clear prompts, and using Planning mode. Once you’ve nailed those, the advanced features will make much more sense.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;AI-assisted development isn’t magic, and it’s not going to replace you. It’s a tool that enables you to focus on solving the actual problem and helps you save time by automating the coding part.&lt;/p&gt;

&lt;p&gt;Start with Planning mode. Just talk to the agent. Break big problems into smaller ones together with the agent. Accept that there’s a learning curve and your performance takes a hit in the beginning. And when it finally clicks, the bottleneck moves from typing to thinking.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Python in 2026: Popular As Ever, Avoided in Production</title>
      <link>http://dev.solita.fi/2026/02/09/python-popularity.html</link>
      <pubDate>Mon, 09 Feb 2026 00:00:00 +0000</pubDate>
      <author>open@solita.fi (Solita Oy)</author>
      <guid>http://dev.solita.fi/2026/02/09/python-popularity</guid>
	  
      <description>&lt;p&gt;I’m biased toward Python for many reasons. I’m also one of the few who’ve watched my own Python scripts place &lt;em&gt;$1.5M&lt;/em&gt; worth of asset orders in my personal brokerage account — but that’s a story for another time. The
point is: I trust Python with systems where mistakes are expensive.&lt;/p&gt;

&lt;p&gt;In this post, I’ll explain how Python became the world’s most popular programming language, yet is often sidelined in enterprise production. Why? And should it be?&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/2026-02-python-pop-prod/balancing-scale.jpg&quot; alt=&quot;Popularity vs. Production&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;popularity&quot;&gt;Popularity&lt;/h2&gt;
&lt;p&gt;Python became the world’s most popular language in &lt;a href=&quot;https://www.tiobe.com/tiobe-index/&quot;&gt;2021&lt;/a&gt;, when it left the last two languages (C and Java) in the dust. Thanks to its ease of use and lower friction, it became the
primary choice in &lt;a href=&quot;https://dl.acm.org/doi/pdf/10.1145/3626252.3630761&quot;&gt;academia&lt;/a&gt; as well as for quick hacks. &lt;a href=&quot;https://harvardonline.harvard.edu/course/cs50s-introduction-programming-python&quot;&gt;Harvard&lt;/a&gt;,
&lt;a href=&quot;https://iblnews.org/story/mits-intro-to-cs-using-python-on-edx-reaches-1-2-million-enrollments&quot;&gt;MIT&lt;/a&gt;,
&lt;a href=&quot;https://collegeparentcentral.com/princeton/online-courses/introduction-to-computer-science-and-programming-using-python/2127148731&quot;&gt;Princeton&lt;/a&gt;,
and even &lt;a href=&quot;https://www.chalmers.se/utbildning/dina-studier/hitta-kurs-och-programplaner/kursplaner/TDA548/&quot;&gt;my local uni&lt;/a&gt; now use Python for their introductory courses. At Google and Amazon, it’s a first-class language,
and the other cloud providers (Microsoft, IBM, Oracle, etc.) fully support it for serverless and their other APIs.&lt;/p&gt;

&lt;p&gt;For many students and professionals — myself included — the low barrier to entry and high productivity, without any safety net (like type checks), is love at first sight. I &lt;em&gt;really&lt;/em&gt; enjoy writing Python code: I’m able
to focus on the problem at hand, and can ignore all formalism. And the freedom! Oh boy, the freedom.&lt;/p&gt;

&lt;p&gt;For ML and AI tasks, Python is the default language. TensorFlow, PyTorch, Pandas, and scikit-learn all effectively require you to jump through hoops to use anything else.&lt;/p&gt;

&lt;p&gt;But what &lt;em&gt;exactly&lt;/em&gt; made it a giant in the first place? It’s trivial; just answer this: how many languages do you know of that can open a browser tab for every HTML file in some subdirectory, only using the standard
library and a couple of lines of code?&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-python3&quot;&gt;import pathlib, webbrowser

for htmlfile in pathlib.Path(&quot;.&quot;).rglob(&quot;*.html&quot;):
    webbrowser.open(htmlfile.absolute().as_uri())
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Some will say that’s compact and convoluted. I say brief and beautiful! But let’s not get bogged down by aesthetics here. No main function. No error checking. No type checking. Just. Make. It. Happen.&lt;/p&gt;

&lt;p&gt;Quite the opposite of what an &lt;a href=&quot;https://en.wikipedia.org/wiki/Architecture_astronaut&quot;&gt;Architecture Astronaut&lt;/a&gt; would come up with: this has nothing to add, nothing to subtract, and if you understand those two lines of
code, you know everything there is. No interfaces, no objects to orient, no errors to handle, no types to take care of, no main to declare. A noob who sees it can understand it — it’s almost English. And this
simplicity, of course, not only applies to browsers and files. For &lt;em&gt;All The Things&lt;/em&gt;, Python makes it easier.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/2026-02-python-pop-prod/up-up-up-up-up-uu-uupp.jpg&quot; alt=&quot;Rise up&quot; /&gt;&lt;/p&gt;

&lt;p&gt;And let’s not forget the feeling. I can script &lt;em&gt;anything&lt;/em&gt; in a few lines; that’s addictive!&lt;/p&gt;

&lt;h2 id=&quot;to-use-or-not-to-use&quot;&gt;To Use or Not To Use&lt;/h2&gt;
&lt;p&gt;When building desktop and mobile apps, Python is not where sane people tend to look. Performance is equally off-brand. Even when it comes to web dev, Python still suffers from a bad rep (arguably due to the horrific
standard library web server implementation). There have however, been stellar industry-standard web frameworks for &lt;a href=&quot;https://flask-dev.readthedocs.io/en/latest/changelog.html#version-0-1&quot;&gt;16 years&lt;/a&gt;. Assuming you’ve
created the app in a couple of lines, this is how easy it is to serve an HTML page:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-python3&quot;&gt;@app.route(&quot;/&quot;)
def index():
    return '&amp;lt;a href=&quot;https://tinyurl.com/iamdeveloper&quot;&amp;gt;Goodnight!&amp;lt;/a&amp;gt;'
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;My two code examples here are not outliers; Python is generally extremely concise and productive, including for web dev.&lt;/p&gt;

&lt;p&gt;What’s more, many performance problems are &lt;a href=&quot;https://numba.pydata.org/&quot;&gt;fixable&lt;/a&gt;. I’m one of the few who should know, as I’ve written an online 3D &lt;a href=&quot;https://store.steampowered.com/app/3462870/Landslip/&quot;&gt;game&lt;/a&gt; with
physics, skinning, and terrain generation. In Python. From scratch. Able to reach &lt;a href=&quot;https://www.youtube.com/watch?v=oLDnQELxKsM&quot;&gt;500 FPS&lt;/a&gt; on my old junk PC. (That’s a whole ‘nother blog post.) As you see, the performance
ceiling isn’t where many think.&lt;/p&gt;

&lt;p&gt;In regards to type information, error handling, and hygiene, most risks can be mitigated with a &lt;a href=&quot;https://github.com/astral-sh/ruff&quot;&gt;linter&lt;/a&gt; and a &lt;a href=&quot;https://github.com/microsoft/pyright&quot;&gt;static type checker&lt;/a&gt; (pre-commit
time).&lt;/p&gt;

&lt;p&gt;If Python &lt;em&gt;can&lt;/em&gt; do all these things — and easily too — why is nobody using it for enterprise production?&lt;/p&gt;

&lt;h2 id=&quot;when-its-strengths-become-its-weaknesses&quot;&gt;When its strengths become its weaknesses&lt;/h2&gt;
&lt;p&gt;Python is made for scripting, and it’s super easy to get started. But if you are used to letting the IDE take care of most problems at the time of writing code, there’s a real risk of runtime problems unless guardrails
are put in place. And frankly, most humans are lazy. Good programmers more than most. So while it’s easy to fix, those that don’t know, don’t know — and don’t want to know. An
&lt;a href=&quot;https://arxiv.org/pdf/1409.0252&quot;&gt;old study&lt;/a&gt; from 2015 shows Python to be one of the most concise languages, but also one of the most error-prone. The quick-hack qualities that propelled its popularity also make it easy
to misuse at scale, which in turn fuels enterprise distrust.&lt;/p&gt;

&lt;p&gt;When it comes to data scientists, analysts, and researchers, they aim for experimentation and insights. Their incentives have nothing to do with long-term maintainability for large teams. So we can’t take their advice on
what to code our enterprise systems in.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://survey.stackoverflow.co/2023/#most-popular-technologies-language-prof&quot;&gt;Half&lt;/a&gt; of professional developers (have to?) &lt;em&gt;use&lt;/em&gt; Python, but it seems highly disliked by those who don’t take it to heart. Looking at my
own employer, only &lt;a href=&quot;https://dev.solita.fi/2024/12/03/developer-survey-2024.html&quot;&gt;6% prefer Python&lt;/a&gt; over other languages. From talking to developers, I believe that the main reason for the dislike is that people feel
it’s “like Java but error-prone and slow.” To which the response should be: &lt;a href=&quot;https://www.youtube.com/watch?v=A-RfHC91Ewc&quot;&gt;if my grandmother had wheels, she would have been a bike&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But of course, there is a kernel of truth to that. On the other hand, nobody avoids knives because “that’s what Jack the Ripper used.” I say: better to learn how to use a knife, and use it with caution. And I realize
that no amount of preaching is going to move the needle even one bit. Which also tells me that you, dear choir, who’ve made it this far are part of the few Chosen Ones.&lt;/p&gt;

&lt;h2 id=&quot;causality-vs-python-10&quot;&gt;Causality vs. Python: 1–0&lt;/h2&gt;
&lt;p&gt;We all know Python is used by everyone and their Uber driver. &lt;a href=&quot;https://www.python.org/about/success/usa/&quot;&gt;NASA&lt;/a&gt; uses it, &lt;a href=&quot;https://www.fullstackpython.com/companies-using-python.html&quot;&gt;banks&lt;/a&gt; use it,
&lt;a href=&quot;https://github.com/reddit?language=python&quot;&gt;Reddit&lt;/a&gt;’s built on it. Everyone from &lt;a href=&quot;https://www.superprof.co.uk/blog/what-companies-use-python/&quot;&gt;Netflix to Pfizer&lt;/a&gt; uses it for orchestration and analysis. But. And there’s a
big but. The enterprise adoption has to do with the arrow of time. Let me explain.&lt;/p&gt;

&lt;p&gt;Nobody who’s deciding on a language for their platform:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Spends years learning Python,&lt;/li&gt;
  &lt;li&gt;Discovers how productive it is,&lt;/li&gt;
  &lt;li&gt;Learns about static type checkers,&lt;/li&gt;
  &lt;li&gt;Learns how to fix performance bottlenecks,&lt;/li&gt;
  &lt;li&gt;Builds confidence that it can work in enterprise production,&lt;/li&gt;
  &lt;li&gt;And then goes back in time to pick Python as their foundation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By the time you’ve climbed that ladder of understanding, the choices have already been made. Python is the chicken, and nobody’s the egg.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/2026-02-python-pop-prod/ladder-of-understanding.jpg&quot; alt=&quot;The ladder of understanding&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Many people realize Python is highly productive, but very few know it can be highly performant. Most people get off the ladder way before they see that Python can be suitable for production.&lt;/p&gt;

&lt;p&gt;Even MSc graduates who’ve used Python a lot for experimentation in small teams have no prior experience in working in large corporations with enterprise solutions. And surely they’ve all made abominable programming
mistakes during their years in college, so they’d be the first to admit that Python is too permissive for its own good. And a lab exercise in computer science is a far cry from a 100-person development department where
everyone is trying to churn out features.&lt;/p&gt;

&lt;p&gt;For the staggering few remaining proponents, there’s one more hurdle: they hear that YouTube, Instagram, and Dropbox rewrote parts of their stack in other languages for performance. Counterfactual reasoning is hard.
Would YouTube have been written by 2 people in a small office above a pizzeria if it weren’t for Python? Who knows.&lt;/p&gt;

&lt;p&gt;So the enterprise reasoning goes “better safe than sorry.” And truth be told, that might be the right way to go.&lt;/p&gt;

&lt;h2 id=&quot;final-words-on-enlightenment&quot;&gt;Final words on enlightenment&lt;/h2&gt;
&lt;p&gt;But if you’re the type of company that dares to think different — if you’ve gone all the way up that ladder, eaten the egg, and reversed the arrow of time — then, just like a Buddhist monk, you’ll have focused your ass
off for years, just to realize when you pop out at the top of enlightenment, that it was all &lt;em&gt;so easy&lt;/em&gt; all along.&lt;/p&gt;

&lt;p&gt;And then, just like the monk, you’re in for a &lt;em&gt;really&lt;/em&gt; good time. Aesthetically and productively. And more than ever, you’ll be able to focus on the big picture, not on the code.&lt;/p&gt;

&lt;p&gt;But &lt;em&gt;definitely&lt;/em&gt; don’t take my word for it. Love is blind, you know.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclaimer: The content, ideas, and the way they’re expressed in this blog post are entirely my own. However, Solita FunctionAI was utilized for grammar, spelling, and stylistic review, and ChatGPT was used for
visualizations.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Decoupling a Legacy Spring Boot Monolith with Branch by Abstraction</title>
      <link>http://dev.solita.fi/2026/01/08/decoupling-legacy-monolith.html</link>
      <pubDate>Thu, 08 Jan 2026 00:00:00 +0000</pubDate>
      <author>open@solita.fi (Solita Oy)</author>
      <guid>http://dev.solita.fi/2026/01/08/decoupling-legacy-monolith</guid>
	  
      <description>&lt;p&gt;We’ve been developing and maintaining a Java Spring Boot codebase for the past 15+ years. In the beginning, there was one server (let’s call it Foo) and a client UI application. Over the years, this codebase has evolved into a monorepo containing four client UI applications and two server applications (Foo and Bar), all of which share the same database schema.&lt;/p&gt;

&lt;p&gt;When Bar was introduced, a decision was made to share code (Spring components) directly from the Foo codebase. Even worse, Bar was directly coupled to Foo database schema. Now the older Foo application is finally near end-of-life and it should be replaced by a new application. However, with Bar directly dependent on Foo’s code and database, the question was: how do we achieve this replacement without breaking everything? To make matters worse, all applications are critical with 24/7 on-call support.&lt;/p&gt;

&lt;p&gt;In this post I’ll dive deep into how our team tackled the challenge in decoupling the applications by systematic refactoring. As the work is still ongoing, I’ll focus on changes related to a single business domain. I’ll share lessons learned on managing risk, verifying quality, and keeping developer morale high while making large-scale changes to a critical system.&lt;/p&gt;

&lt;h2 id=&quot;here-there-everywhere&quot;&gt;Here, There, Everywhere&lt;/h2&gt;
&lt;p&gt;Our initial task was to identify how the business domain (which we’ll call “Product” throughout this post), conceptually owned by the Foo application, was being utilized within Bar. What we quickly uncovered was a sprawling mess: a multitude of Product-related domain objects, DTOs, JPA repositories, Hibernate DAOs, named queries, and various other data access mechanisms, all scattered across both Foo and Bar, accumulated over years of development.&lt;/p&gt;

&lt;p&gt;Whenever the Bar application needed to interact with “Product,” it seemingly &lt;em&gt;randomly&lt;/em&gt; selected one of these services, repositories, or DAOs. There were no clear boundaries, no separation of concerns, and no unified interfaces. We were looking at hundreds of integration ‘seams’ where the Product entity was accessed, either directly in code or through database queries. Crucially, the Product entity’s influence wasn’t confined to the data access layer; its database identifiers and related logic were pervasive throughout the codebase.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/2026-01-decoupling-legacy-monolith/sprawling_mess.png&quot; alt=&quot;Our codebase was a sprawling mess&quot; /&gt;&lt;/p&gt;

&lt;p&gt;To navigate this complexity, static analysis tools within our preferred editor, IntelliJ IDEA (specifically ‘Find Usages’ of classes and methods, and dependency graphs), proved invaluable. Our ultimate goal was to transition Bar to consume a dedicated &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductAPI&lt;/code&gt;, exposed as a new HTTP REST service for all Product information access. But how could we get there? The chasm between our current state and that desired future seemed insurmountable.&lt;/p&gt;

&lt;h2 id=&quot;our-strategy-branch-by-abstraction&quot;&gt;Our Strategy: Branch by Abstraction&lt;/h2&gt;
&lt;p&gt;Martin Fowler defines the &lt;a href=&quot;https://martinfowler.com/bliki/BranchByAbstraction.html&quot;&gt;Branch by Abstraction&lt;/a&gt; pattern as:&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;…a technique for making a large-scale change to a software system in gradual way that allows you to release the system regularly while the change is still in-progress.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is especially powerful in critical systems when we must release changes gradually, ensuring the quality of the overall system and allowing multiple implementations to coexist simultaneously in production.&lt;/p&gt;

&lt;p&gt;Here’s how Branch by Abstraction typically unfolds through its core phases:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1: The Starting Point&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At the starting point, we have clients calling a poorly behaving supplier:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/2026-01-decoupling-legacy-monolith/branch_by_abstraction_phase_1.png&quot; alt=&quot;Phase 1: The Starting Point&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2: Introducing the Abstraction and Gradual Migration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We introduce a new abstraction layer (an interface) between the client and the existing supplier. We then gradually &lt;strong&gt;migrate&lt;/strong&gt; client calls to go through this abstraction layer, piece by piece:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/2026-01-decoupling-legacy-monolith/branch_by_abstraction_phase_2.png&quot; alt=&quot;Phase 2: Abstraction Layer&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 3: Full Abstraction Adoption&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Eventually, all client calls have been successfully migrated to go through the abstraction layer:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/2026-01-decoupling-legacy-monolith/branch_by_abstraction_phase_3.png&quot; alt=&quot;Phase 3: Full Abstraction Adoption&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 4: Building the New Supplier&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Next, we build the new, modern supplier, ensuring it fully implements the newly introduced abstraction layer:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/2026-01-decoupling-legacy-monolith/branch_by_abstraction_phase_4.png&quot; alt=&quot;Phase 4: New supplier&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 5: Gradual Cutover to the New Supplier&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We then start gradually switching clients to use the new supplier’s implementation of the abstraction. This critical phase can often be managed with feature flags or A/B testing:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/2026-01-decoupling-legacy-monolith/branch_by_abstraction_phase_5.png&quot; alt=&quot;Phase 5: Gradual cutover&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 6: Removing the Old Supplier&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once all clients have been migrated to use the new supplier, the old, poorly behaving supplier (and its implementation behind the abstraction) can be safely removed:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/2026-01-decoupling-legacy-monolith/branch_by_abstraction_phase_6.png&quot; alt=&quot;Phase 5: Remove old supplier&quot; /&gt;&lt;/p&gt;

&lt;p&gt;In the intricate dance of large-scale refactorings, a core challenge lies in balancing delivery speed against the inherent risks of downtime and critical bugs. Given our critical environment with 24/7 on-call support, we deliberately prioritized minimizing risk, understanding that this would inherently mean a more measured pace for feature delivery to production. Branch by Abstraction, coupled with the rigorous quality verification techniques detailed later in this post, became our fundamental approach for ensuring this low-risk posture. This prioritization, however, naturally extended the overall timeline for the full migration, a conscious trade-off we embraced.&lt;/p&gt;

&lt;p&gt;With Branch by Abstraction as our guiding principle, our first concrete application began with the ‘Product’ domain, aiming to create that initial abstraction layer.&lt;/p&gt;

&lt;h2 id=&quot;introducing-the-abstraction-layer-productservice&quot;&gt;Introducing the Abstraction Layer: ProductService&lt;/h2&gt;
&lt;p&gt;To bring structure to this chaos, our first crucial step was to introduce a unified access layer for “Product” data within Foo. This meant defining a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductService&lt;/code&gt; interface, which would become the &lt;em&gt;sole&lt;/em&gt; conduit for Bar’s access to Product information. A key design decision was that this interface must not expose underlying Hibernate-proxied entities, but rather pure &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductDto&lt;/code&gt; objects.&lt;/p&gt;

&lt;p&gt;We then implemented a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FooProductServiceImpl&lt;/code&gt; for this interface. Crucially, this implementation still leveraged the existing Foo services, repositories, and DAOs for data access. However, by funneling all Bar’s interactions through the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductService&lt;/code&gt; interface, we effectively decoupled Bar from Foo’s internal implementation details.&lt;/p&gt;

&lt;p&gt;To handle the conversion from existing entities to the new DTOs, we integrated the &lt;a href=&quot;https://mapstruct.org&quot;&gt;MapStruct&lt;/a&gt; library into our codebase. This allowed us to gradually replace direct &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Product&lt;/code&gt; entity access in Bar with calls to the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductService&lt;/code&gt; interface, returning &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductDto&lt;/code&gt; objects. This initial refactoring was relatively straightforward and easy to split into smaller tasks, primarily because the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductDto&lt;/code&gt; objects, at this stage, mirrored the data and fields of the original entities. However, we were able to simplify &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductDto&lt;/code&gt; significantly, because there were quite a few fields or methods that the Bar application didn’t need.&lt;/p&gt;

&lt;p&gt;It’s important to note that, at this point, we deliberately didn’t over-optimize the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductService&lt;/code&gt; interface’s structure or cohesion. It contained numerous methods with slightly differing parameters for retrieving Product data. The more complex task of rationalizing, removing duplicates, and condensing the interface into a leaner, more cohesive design was deferred to a later project phase.&lt;/p&gt;

&lt;p&gt;Finally, to enforce this architectural boundary and prevent regressions, we introduced architecture tests using &lt;a href=&quot;https://www.archunit.org&quot;&gt;ArchUnit&lt;/a&gt;. These tests ensured that the Bar codebase could no longer directly reference or use the original &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Product&lt;/code&gt; entity, solidifying our new decoupling strategy.&lt;/p&gt;

&lt;h2 id=&quot;decoupling-product-data-from-the-database&quot;&gt;Decoupling Product Data from the Database&lt;/h2&gt;
&lt;p&gt;Once the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductService&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductDto&lt;/code&gt; abstraction layers were established, our next major undertaking was to decouple the Bar application’s code from the shared database. At this stage, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductDto&lt;/code&gt; still contained its internal database identifier, and it often referenced other Foo domain objects (like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IngredientDto&lt;/code&gt;) using their database identifiers as well. We recognized that relying on such internal database details was not only poor practice but would also be impossible once we transitioned to the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductAPI&lt;/code&gt; service.&lt;/p&gt;

&lt;p&gt;Our solution involved systematically replacing these database identifiers with more stable, business-oriented identifiers. This transition was, once again, implemented gradually within the Bar codebase:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;First, the business identifier was introduced into the relevant DTO objects.&lt;/li&gt;
  &lt;li&gt;Next, Bar’s code was updated to utilize this new business identifier.&lt;/li&gt;
  &lt;li&gt;Finally, once a database identifier was no longer referenced anywhere in Bar’s code, it could be safely removed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A second significant challenge was Bar’s direct database access, a consequence of the shared schema. Numerous queries within Bar involved JOINs directly to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PRODUCT&lt;/code&gt; table or its related tables. This, too, represented an anti-pattern and would be unsustainable after switching to the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductAPI&lt;/code&gt; service.&lt;/p&gt;

&lt;p&gt;To address this, we systematically refactored Bar’s codebase to route all Product-related data access through the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductService&lt;/code&gt; abstraction layer. The common pattern that emerged involved a Bar service layer first fetching its own Bar-specific domain objects via its repository layer. It would then determine what Product data was needed and subsequently fetch that information using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductService&lt;/code&gt; abstraction layer, effectively orchestrating data from both sources.&lt;/p&gt;

&lt;h2 id=&quot;gradual-switchover-to-the-productapi&quot;&gt;Gradual Switchover to the ProductAPI&lt;/h2&gt;
&lt;p&gt;With the Bar codebase now entirely decoupled from direct database queries for Product-related data, and our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductDto&lt;/code&gt; and related DTOs cleaned of all internal database identifiers, we were finally ready to introduce a second implementation of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductService&lt;/code&gt; interface representing our new modern supplier.&lt;/p&gt;

&lt;p&gt;This new implementation, aptly named &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductApiServiceImpl&lt;/code&gt;, was designed to fetch Product data from the external &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductAPI&lt;/code&gt; via its HTTP REST interface. Since the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductAPI&lt;/code&gt; provided an OpenAPI description, we were able to leverage this to automatically generate a robust Java client. This generated client was then injected into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductApiServiceImpl&lt;/code&gt; and further enhanced with &lt;a href=&quot;https://resilience4j.readme.io&quot;&gt;Resilience4j&lt;/a&gt; features, including circuit breakers and retry functionality, to gracefully mitigate network transient errors and prevent API overload. As before, MapStruct played a vital role in seamlessly converting the data from the generated &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductAPI&lt;/code&gt; client classes into our established &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductDto&lt;/code&gt; objects.&lt;/p&gt;

&lt;p&gt;At this juncture, we had two distinct implementations of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductService&lt;/code&gt; interface. The existing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FooProductServiceImpl&lt;/code&gt; (using the old Foo internals) was designated as the default using Spring’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Primary&lt;/code&gt; annotation. To manage the gradual transition, we began switching Bar’s services, one by one, to utilize the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductApiServiceImpl&lt;/code&gt; by selectively injecting it using Spring’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Qualifier&lt;/code&gt; annotation. This allowed for fine-grained control over which parts of Bar began consuming data from the new API, enabling a safe and incremental cutover.&lt;/p&gt;

&lt;h2 id=&quot;quality-verification&quot;&gt;Quality Verification&lt;/h2&gt;

&lt;p&gt;When working with critical software that demands 24/7 on-call support, investing in rigorous quality verification is paramount. We were undertaking large-scale changes to the very core of the Bar application, and while most pull requests were of a manageable size, some were substantial. In total, this effort spanned over a hundred Jira issues. The pressing question was: how do we thoroughly verify quality to prevent those dreaded late-night calls to the on-call duty officer?&lt;/p&gt;

&lt;p&gt;Our strategy, beyond maintaining good overall test coverage in new code, encompassed feature flags, A/B testing with result verification, and performance testing. We leveraged feature flags to dynamically switch between old and new implementations, for instance, during the migration from database identifiers to business-oriented ones. This mechanism provided a rapid rollback capability in production if any issues emerged, greatly reducing risk.&lt;/p&gt;

&lt;p&gt;A/B testing with result verification proved to be another heavily utilized technique. For this, we relied on a custom implementation that was fortunately already available. Our team had previously developed this robust testing facility during an earlier migration from legacy Hibernate code. This A/B testing facility was designed to always return the result of the &lt;em&gt;existing&lt;/em&gt; implementation, while simultaneously executing the &lt;em&gt;new&lt;/em&gt; implementation and verifying its results against the old. We could precisely control the percentage of requests that executed the new implementation, mitigating potential high-load issues. The facility also supported custom verifiers, which proved essential since simple equality verification wasn’t always feasible. We conducted A/B testing in both our test environments and in production. Once we gained sufficient confidence that no discrepancies existed between the old and new implementation results, the A/B testing harness could be safely removed from the codebase.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/2026-01-decoupling-legacy-monolith/ab_tester.png&quot; alt=&quot;A/B testing flow&quot; /&gt;&lt;/p&gt;

&lt;p&gt;For performance testing, we already utilized Locust to simulate the load of tens of client applications. While we didn’t specifically performance-test each new implementation in isolation, we rigorously checked for any regressions in overall performance test results between release candidates.&lt;/p&gt;

&lt;p&gt;In addition to verifying functional correctness, our A/B testing facility also proved invaluable for &lt;strong&gt;performance analysis&lt;/strong&gt;. By logging the execution times for both old and new implementation calls, we could meticulously inspect differences in performance. Given that the old implementation relied on direct database access while the new one fetched data over HTTP from an external service, inherent differences in access methods were expected. We observed a mixed bag: some data fetches were surprisingly faster with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductAPI&lt;/code&gt; implementation, particularly when the old SQL query was complex. However, others — especially for simple SQL queries fetching small datasets — were significantly slower through the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductAPI&lt;/code&gt;. This disparity immediately highlighted the need for a caching solution to prevent &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Product&lt;/code&gt; data access from becoming a performance bottleneck. We began with a simple in-memory cache for each server instance, strategically caching frequently accessed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Product&lt;/code&gt; instances. This foundational cache is designed for potential future migration to more robust solutions like Redis/Valkey, if needed. Furthermore, we identified opportunities for HTTP-level caching for larger datasets from the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductAPI&lt;/code&gt;, and in some cases, we even refactored data access patterns to proactively gain performance benefits during the migration.&lt;/p&gt;

&lt;h2 id=&quot;where-we-are-now&quot;&gt;Where We Are Now&lt;/h2&gt;

&lt;p&gt;We’ve successfully introduced the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductAPI&lt;/code&gt;-based implementation into production for a significant portion of Bar services. While a few issues were inevitably discovered only in production, our robust A/B testing facility proved invaluable. These issues were promptly identified via production logging and monitoring, and crucially, as the facility always returned results from the old implementation, there were no visible problems or service interruptions for our clients.&lt;/p&gt;

&lt;p&gt;We acknowledge that a substantial migration effort still lies ahead to transition all remaining Bar services to the new implementation, but we are confident in achieving this goal soon. We anticipate that subsequent service migrations will be relatively more straightforward, benefiting from the lessons learned and issues resolved during the initial cutovers and A/B testing phases. For these later migrations, the need for extensive A/B testing might diminish, allowing for direct migration to the new implementation. This, of course, represents a critical trade-off between delivery speed and risk management, necessitating a careful, case-by-case evaluation.&lt;/p&gt;

&lt;p&gt;Throughout this extensive process, celebrating small, yet significant, victories has been crucial for maintaining team morale and momentum. These milestones include:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Ensuring the entire Bar codebase now exclusively utilizes the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductService&lt;/code&gt; abstraction layer.&lt;/li&gt;
  &lt;li&gt;Implementing architecture tests to prevent the reintroduction of the old &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Product&lt;/code&gt; entity in the Bar application.&lt;/li&gt;
  &lt;li&gt;Eliminating all direct database queries within the Bar codebase that previously performed SQL JOINs to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PRODUCT&lt;/code&gt; table.&lt;/li&gt;
  &lt;li&gt;The first successful production migration of a Bar service to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProductAPI&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;key-takeaways-and-lessons-learned&quot;&gt;Key Takeaways and Lessons Learned&lt;/h3&gt;

&lt;p&gt;Our journey through this large-scale refactoring provided several invaluable lessons that are applicable to similar complex projects:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Branch by Abstraction is Indispensable for Critical Systems:&lt;/strong&gt; For high-traffic, continuously deployed applications, the incremental, low-risk nature of Branch by Abstraction is a powerful enabler for significant architectural changes without disruption.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Enforcement through Architectural Tests is Key:&lt;/strong&gt; Tools like ArchUnit are not just helpful; they are essential for preventing regressions and maintaining newly established architectural boundaries. Without them, the carefully built seams can easily fray.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Proactive Resilience Pays Off:&lt;/strong&gt; Integrating resilience patterns (like Resilience4j’s circuit breakers and retries) from the outset for new external API calls is non-negotiable. It protects the system from transient network issues and external service instability.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Custom A/B Testing Facilities are Strategic Assets:&lt;/strong&gt; For deep, internal refactoring where external tools might not suffice, investing in a custom, controlled A/B testing harness that can verify results &lt;em&gt;in production&lt;/em&gt; while shielding end-users is incredibly powerful for de-risking deployments.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Small Victories Fuel Long Journeys:&lt;/strong&gt; Large, multi-month projects can be draining. Recognizing and celebrating intermediate achievements, even seemingly minor ones, keeps the team engaged, motivated, and focused on the long-term goal. It can be as simple as bubbles or cinnamon rolls in team retrospective.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Continuous Evaluation of Trade-offs:&lt;/strong&gt; Even with a clear strategy, the path evolves. Regularly reassessing the risk-vs-speed trade-off (e.g., for subsequent A/B testing) ensures agility and efficiency.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This refactoring has significantly modernized our architecture, improved our data access patterns, and laid a solid foundation for future development, all while maintaining the stability required by a critical 24/7 service.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclaimer: The content and ideas expressed in this blog post are entirely my own. However, Solita FunctionAI was utilized for grammar, spelling and stylistic review, and ChatGPT was used for visualizations.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Speed Up Data Processing in Node with p-limit and Worker Threads</title>
      <link>http://dev.solita.fi/2025/11/18/speed-up-node-data-processing.html</link>
      <pubDate>Tue, 18 Nov 2025 00:00:00 +0000</pubDate>
      <author>open@solita.fi (Solita Oy)</author>
      <guid>http://dev.solita.fi/2025/11/18/speed-up-node-data-processing</guid>
	  
      <description>&lt;p&gt;When I started writing this blog post, I realized that Node.js is now a teenager — at that age I was setting up my first website with PHP and only &lt;em&gt;thinking&lt;/em&gt; about becoming a software developer. Back then, Node.js was created to let web servers use JavaScript, the same language powering websites. While Node.js is well-suited for I/O driven web servers, it faces challenges with CPU-intensive computations due to its single-threaded event loop. When heavy computations run on the main thread, they can block other events and degrade overall performance.&lt;/p&gt;

&lt;p&gt;In this blog post, we’ll explore practical strategies to accelerate a real-world Node application that reads several large JSON files from disk, processes their contents, and outputs the results as a large number of smaller JSON files. We’ll introduce two powerful Node.js tools: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p-limit&lt;/code&gt; library for managing concurrent I/O operations, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;worker_threads&lt;/code&gt; for parallelizing CPU-bound tasks. By combining these techniques, we can significantly reduce processing time and make Node applications more scalable.&lt;/p&gt;

&lt;h2 id=&quot;limiting-factors-in-node&quot;&gt;Limiting Factors in Node&lt;/h2&gt;

&lt;p&gt;To make sense of how to optimize performance in Node.js, it is essential to first understand the underlying limitations. At its core, Node.js is built on the V8 JavaScript engine, which provides high-speed execution of JavaScript code, but even highly optimized code cannot match the speed of native code. This means that Node.js is hardly the fastest tool available for raw data processing, so why use it for that in the first place? Well, it is a natural choice for handling JSON-based data due to its native support for JavaScript and rich node_modules ecosystem. It’s also a widely used tool and can be easily picked up by virtually any web developer.&lt;/p&gt;

&lt;p&gt;Node’s architecture is fundamentally event-driven and single-threaded. This works fine for most web interfaces and servers, but can be seen as a limitation for pure data processing. In simple terms, single-threaded means that only a single JavaScript event or function can be on execution at a time. Even if you put your code in a Promise-returning function, its synchronous parts are still run sequentially and block the event loop from executing other tasks. Luckily, Node.js is still efficient for handling I/O operations such as reading/writing files and handling network traffic. These operations are delegated to run by the underlying system (mainly &lt;strong&gt;libuv thread pool&lt;/strong&gt;) and thus do not overload Node’s main event loop.&lt;/p&gt;

&lt;p&gt;To optimize our Node process, we are going to maximize the performance of parallel I/O operations and also parallelize CPU-bound operations. How much of an increase can we expect? It’s difficult to give a precise prediction, as it largely depends on the use case. In our case, we start with a processing time of &lt;strong&gt;about 40 seconds&lt;/strong&gt;. Let’s see how much we can improve it.&lt;/p&gt;

&lt;h2 id=&quot;processing-data-overview&quot;&gt;Processing Data Overview&lt;/h2&gt;

&lt;p&gt;Let’s begin with a simple example of processing data and writing the result to the disk:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;file&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;of&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;files&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;processData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;promises&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;writeFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// I/O operation, handled by libuv&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Each file is processed and written to the disk one-by-one. This is simple and straightforward, and if it’s fast enough for you, you could stop reading here. Otherwise, we can see that this code is not very efficient since we are only either processing data &lt;strong&gt;or&lt;/strong&gt; waiting for the write operation to complete. Let’s improve it by running the write operation in the background:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;file&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;of&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;files&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;processData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;fs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;promises&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;writeFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// no await here, let it run in background&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here we do not wait for file writing to complete, but schedule it and immediately continue processing the next file. However, error checking is still missing and we do not wait for those background operations to complete. Let’s fix it:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;writePromises&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[];&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;file&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;of&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;files&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;processData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;p&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;promises&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;writeFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;writePromises&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Data processing done, wait for all write operations to successfully complete. This throws an exception if even one write failed.&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;writePromises&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This looks better from performance perspective as we are now doing more than one thing at a time, but the gained performance highly depends on how fast &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;processData&lt;/code&gt; is compared to file writing. If it’s slow, we might not gain much benefit at all. We also have a potential memory problem since we are queuing up all write operations to &lt;strong&gt;libuv&lt;/strong&gt; at once. If &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;result&lt;/code&gt; is large and there are many files to process, this could lead to high memory peaks. While &lt;strong&gt;libuv&lt;/strong&gt; has some limits of how many write operations it does in parallel by default, the remaining operations are still put into queue. Let’s see if we can improve this further using &lt;strong&gt;p-limit&lt;/strong&gt;.&lt;/p&gt;

&lt;h2 id=&quot;p-limit&quot;&gt;p-limit&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/sindresorhus/p-limit&quot;&gt;p-limit&lt;/a&gt; is a lightweight library for controlling concurrency in asynchronous operations. In other words, it can be used to easily limit the number of I/O-bound promises running simultaneously. The emphasis is on the word &lt;strong&gt;I/O-bound&lt;/strong&gt;; p-limit does not help with CPU-bound tasks since it does not magically make JavaScript multi-threaded.&lt;/p&gt;

&lt;p&gt;Going back to our previous example, what we actually want to do is to run multiple file write operations in parallel and in the background, but avoid running &lt;em&gt;too many&lt;/em&gt; to cause high memory peaks. &lt;strong&gt;p-limit&lt;/strong&gt; solves this for us by allowing to specify the maximum number of concurrently run promises.&lt;/p&gt;

&lt;p&gt;The right amount of concurrent operations depends on the use case and the capability of the underlying hardware. I would suggest beginning with a value of 2-6 and finding the sweet spot manually by testing with real-world data.&lt;/p&gt;

&lt;p&gt;Let’s take a look at how to use &lt;strong&gt;p-limit&lt;/strong&gt;, first doing it &lt;strong&gt;incorrectly&lt;/strong&gt;.&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;plimit&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;p-limit&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;limit&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;plimit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Allow up to 4 concurrent operations&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[];&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Naive example: assuming that files come from somewhere and `processFile` is implemented somewhere&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;file&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;of&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;files&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;processData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// BAD! Memory peak can still occur here.&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;task&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;limit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;promises&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;writeFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Wait for all writes to complete&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In this example, we are limiting the total number of simultaneous file operations, but we are NOT limiting calls to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;processData&lt;/code&gt;. This means that eventually all finished &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;result&lt;/code&gt; objects would be queued to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p-limit&lt;/code&gt;, potentially causing high memory peaks if the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;result&lt;/code&gt; object is large.&lt;/p&gt;

&lt;p&gt;The can be solved by limiting both data processing and file writing:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;plimit&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;p-limit&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;limit&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;plimit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[];&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;file&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;of&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;files&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;task&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;limit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;processData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;promises&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;writeFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Wait for all processing + writes to complete&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This is starting to look good as we are now allowing parallel data processing and file writing while still setting a limit of how many &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;result&lt;/code&gt; objects can be created at once. Again, the gained performance largely depends on the relative speed of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;processData&lt;/code&gt; and file writing. In our case, this trick decreased the processing time &lt;strong&gt;from 40 seconds to about 30 seconds&lt;/strong&gt;.&lt;/p&gt;

&lt;h2 id=&quot;p-limit-with-pending-queue&quot;&gt;p-limit with Pending Queue&lt;/h2&gt;

&lt;p&gt;Let’s assume there are so many files to be processed that we need to divide them into multiple different processing functions. This would mean that we needed to use p-limit on multiple different places, possibly losing a track of overall concurrency and memory usage. Each &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;plimit&lt;/code&gt; instance only controls the tasks submitted to it, not the total number of tasks running across all instances. This can easily lead to more tasks executing simultaneously than intended.&lt;/p&gt;

&lt;p&gt;To manage this safely, we could use a single global concurrency limiter that all processing functions use, ensuring the total number of active heavy tasks never exceeds your safe threshold. Note that this method might not give actual performance benefits, but it helps to keep memory usage more predictable across the whole application.&lt;/p&gt;

&lt;p&gt;Here is an example of introducing a centralized write queue using a single instance of &lt;strong&gt;plimit&lt;/strong&gt;. When data has been processed, it can be queued for writing via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;enqueueWriteToFile&lt;/code&gt; function. This queue is allowed to hold no more than &lt;strong&gt;eight&lt;/strong&gt; pending write operations, setting a clear limit for memory usage in the whole application. If the pending queue is full, the caller is forced to wait until there is free capacity available. Finally, up to &lt;strong&gt;four&lt;/strong&gt; write operations are allowed to run in parallel.&lt;/p&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;promises&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fs&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;fs&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;plimit&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;p-limit&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;MAX_ITEMS_IN_PENDING_QUEUE&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Allow up to 8 operations to wait in the queue&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;writePool&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;plimit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Allow up to 4 concurrent operations&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;pendingWrites&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

&lt;span class=&quot;kr&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;DataFile&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;waitForFreeCapacityInPendingQueue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;check&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;writePool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pendingCount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;MAX_ITEMS_IN_PENDING_QUEUE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;undefined&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// Wait until pending queue is free&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;check&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

    &lt;span class=&quot;nx&quot;&gt;check&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;enqueueWriteToFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;DataFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// Main thread waits here if pending queue is full&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;waitForFreeCapacityInPendingQueue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// Schedule the write operation and continue processing&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;task&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;writePool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;writeFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;pendingWrites&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;finally&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;pendingWrites&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;waitForFileWritesToComplete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([...&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pendingWrites&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;file&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;of&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;importantFiles&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;processed&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;processFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;enqueueWriteToFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;processed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;file&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;of&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;moreImportantFiles&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;processed&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;processFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;enqueueWriteToFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;processed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;waitForFileWritesToComplete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We have now solved the problem of running both data processing and file writing in parallel while also avoiding high memory peaks by limiting the number of pending write operations. However, we are still processing data sequentially, one file at a time. To make this more efficient, we are going to look at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;worker_threads&lt;/code&gt; module next.&lt;/p&gt;

&lt;h2 id=&quot;worker-threads&quot;&gt;Worker Threads&lt;/h2&gt;

&lt;p&gt;Node.js has &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;worker_threads&lt;/code&gt; module, which enables true parallelism by running code inside Workers. This practically frees us from the single-threaded limitation of Node. Hooray, all performance problems solved! But there is a catch: running code in a worker is &lt;em&gt;kind of&lt;/em&gt; like running another Node inside Node. Each worker has its own execution context, call stack, heap memory and event loop. This makes worker threads completely separated and also requires some effort to setup.&lt;/p&gt;

&lt;p&gt;Since running a task in a Worker requires its own “Node”, initialising a Worker &lt;em&gt;practically&lt;/em&gt; requires initialising it with its own script. Here is a simplified example:&lt;/p&gt;

&lt;p&gt;First, we create the worker file and import &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;processData&lt;/code&gt; from the main application:&lt;/p&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// worker_types.ts&lt;/span&gt;
&lt;span class=&quot;kr&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;DataFile&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// worker.ts&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;workerData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;parentPort&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;node:worker_threads&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;processData&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;data/process&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;DataFile&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;./worker_types&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;runWorker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// Params from main thread&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;params&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;workerData&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;DataFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;DataFile&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;processData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// Send result back to the main thread&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;parentPort&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;postMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Run the worker&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;runWorker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;parentPort&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;postMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;message&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We can compile this worker using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;esbuild&lt;/code&gt;, with a command like this: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npx esbuild src/worker.ts --bundle --outdir=dist --format=esm --platform=node&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Finally, we call the worker from the main thread:&lt;/p&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// main.ts&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fileURLToPath&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;node:url&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Worker&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;node:worker_threads&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;DataFile&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;worker_types&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;enqueueWriteToFile&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;io/write_queue&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// From previous p-limit example&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;filename&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fileURLToPath&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;meta&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;dirname&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dirname&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;filename&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;runWorker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;DataFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;reject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;worker&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Worker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dirname&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;worker.js&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;workerData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

    &lt;span class=&quot;nx&quot;&gt;worker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Result from worker:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;enqueueWriteToFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

    &lt;span class=&quot;nx&quot;&gt;worker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;reject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;worker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;exit&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;code&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;code&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;reject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`Worker stopped with exit code &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;code&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// Assuming file comes from somewhere&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;workerPromise&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;runWorker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  
  &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Worker started, main thread is still responsive.&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;workerPromise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Worker finished!&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This example shows how to offload a single computation from main thread to a dedicated worker while allowing main thread to be responsive and continue handling other tasks without being blocked. While this method works, it requires us to manually initialise a new Worker every time we want to process something in parallel. Also, the worker must be tuned to handle different data processing tasks. This process can be simplified by using a library called &lt;a href=&quot;https://github.com/josdejong/workerpool&quot;&gt;workerpool&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;worker-pool&quot;&gt;Worker Pool&lt;/h2&gt;

&lt;p&gt;Here is a simplified example of what &lt;strong&gt;workerpool&lt;/strong&gt; looks like. First, we create a worker file which exposes the functions it can execute:&lt;/p&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// worker.ts&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;processData&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;data/process&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;DataFile&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;worker_types&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;workerpool&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;workerpool&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Must use require&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;processDataFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;DataFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;processData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Expose what functions this worker can execute&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;workerpool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;worker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;processDataFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Main looks like this:&lt;/p&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// main.ts&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;workerpool&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;workerpool&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;DataFile&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;worker_types&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;enqueueWriteToFile&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;io/write_queue&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;filename&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fileURLToPath&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;meta&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;dirname&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dirname&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;filename&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Create a worker pool with as many workers as there are logical CPU cores&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;pool&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;workerpool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dirname&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;/worker.js&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;maxWorkers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;cpus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Assuming file comes from somewhere&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;pool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;exec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;processDataFile&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;enqueueWriteToFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// Terminate all workers when everything is done&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;pool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;terminate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;workerpool&lt;/strong&gt; takes the implementation of creating and calling workers to a higher abstraction level, simplifying code and also making it more efficient since &lt;strong&gt;workerpool&lt;/strong&gt; keeps a pool of workers “warm”, ready to be used for offloading multiple long-running computations in the background. No need to manually create them every time.&lt;/p&gt;

&lt;p&gt;In the above examples, we have offloaded only a single data processing task to the worker pool, but naturally we want to process all data in parallel. However, in heavily parallelized applications, we have to be careful of not offloading and queueing too many operations to workers at once and thus (again) introducing potential memory peaks - just like we need to be cautious with queuing too many I/O operations. Thus, I would be careful of calling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pool.exec&lt;/code&gt; for every file at once since it &lt;em&gt;could&lt;/em&gt; cause worker pool queue to overload of file parameters.&lt;/p&gt;

&lt;h3 id=&quot;worker-pool-with-pending-queue&quot;&gt;Worker Pool with Pending Queue&lt;/h3&gt;

&lt;p&gt;If one needs to limit the number of worker tasks to be queued, we would need to use some kind of pending queue to force the main thread to wait for a free worker to be available. A simplified example would look something like this:&lt;/p&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;pendingTasks&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;queuedTasksCount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;waitForFreeWorker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;check&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pendingTasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;NUMBER_OF_WORKERS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;undefined&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;check&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;check&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;enqueueWorkerTask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;WorkerParams&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// Wait until there is a free worker available in the pool.&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;waitForFreeWorker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;task&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;pool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;exec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;pendingTasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  
  &lt;span class=&quot;nx&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;finally&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Worker task completed:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;pendingTasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;waitForWorkerTasksToComplete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([...&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pendingTasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;pool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;terminate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;By using a worker pool and p-limit together, we were able to reduce the total processing time &lt;strong&gt;from original 40 seconds down to about 14 seconds!&lt;/strong&gt; This is a significant improvement, though it comes with increased code complexity and memory usage since each worker has its own memory space and we are keeping processed results in memory until they are written to disk.&lt;/p&gt;

&lt;h2 id=&quot;tips-for-working-with-workers&quot;&gt;Tips for Working with Workers&lt;/h2&gt;

&lt;p&gt;Whether you decide to use &lt;strong&gt;workerpool&lt;/strong&gt; or implement your own solutions, here are a couple of tips I wish I had known when I started working with Workers:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;You are free to import stuff from your main application code to your worker, but everything you import gets compiled in the final &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;worker.js&lt;/code&gt;. This can result in large bundles and potentially unwanted code if not done carefully. For example, if you introduce a worker pool in your main application code, and you accidentally import some code from there in your worker, &lt;strong&gt;every worker gets its own worker pool&lt;/strong&gt;. Probably not what you want. This can be avoided by doing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;import { isMainThread } from &quot;worker_threads&quot;&lt;/code&gt; and checking if &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;isMainThread&lt;/code&gt; is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;false&lt;/code&gt; when ever something should never be imported by a worker.&lt;/li&gt;
  &lt;li&gt;Worker’s JS code must be built separately every time you modify it and always before running tests (I’m not happy remembering spending a day figuring out why some changes in my worker code did not do anything). If you use Vitest, you can use its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setup.ts&lt;/code&gt; file to run code like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;execSync(&quot;npm run build:worker&quot;, { stdio: &quot;inherit&quot; });&lt;/code&gt; to get a fresh worker every time before running tests.&lt;/li&gt;
  &lt;li&gt;Worker threads have their own isolated memory and cannot reference objects from the main thread (okay, there is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SharedArrayBuffer&lt;/code&gt;, but it’s a bit advanced concept). When you instantiate a worker and pass in some parameters, these parameters are &lt;strong&gt;passed by value&lt;/strong&gt; (i.e. structured clone), not by reference. Thus, one needs to be careful about high memory peaks when using workers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;Here is a quick summary of the tools we covered:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;p-limit&lt;/strong&gt; is ideal for managing concurrency in I/O-bound tasks, such as reading or writing files and making HTTP requests. By limiting the number of simultaneous operations, you prevent resource exhaustion and avoid hitting system limits. The code is only slightly more complex than standard async code, making p-limit a practical choice for many I/O-heavy workloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;worker_threads&lt;/strong&gt; is designed for CPU-bound tasks that would otherwise block the event loop, virtually any heavy data processing. Worker threads run in parallel, leveraging multiple CPU cores for true concurrency. However, using worker threads increases code complexity and memory usage, as each worker has its own execution context. Using it also requires some planning of &lt;em&gt;how&lt;/em&gt; to actually divide complex computation into workers effectively and may not be sensible if tasks depend on each other. For heavy parallel data processing, I recommend taking a look at &lt;a href=&quot;https://github.com/josdejong/workerpool&quot;&gt;workerpool&lt;/a&gt; library.&lt;/p&gt;

&lt;p&gt;Both &lt;strong&gt;p-limit&lt;/strong&gt; and &lt;strong&gt;worker_threads&lt;/strong&gt; are powerful tools for improving Node.js performance, but they serve different purposes and come with trade-offs. Even if these tools allow us to maximize performance by running things in parallel, one needs to be mindful of not fulfilling memory with a queue of tasks or cpu with too many simultaneous workers. Using either or both tools always increases code complexity and memory usage, potentially crashing your application if not managed carefully. Thus, it’s important to evaluate whether the performance gains justify these costs for your specific use case.&lt;/p&gt;
</description>
    </item>
    

  </channel>
</rss>
