<?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>Fri, 20 Feb 2026 09:55:45 +0000</pubDate>
    <lastBuildDate>Fri, 20 Feb 2026 09:55:45 +0000</lastBuildDate>

    
    <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>Using Model-View-ViewModel pattern for UI development in 2025</title>
      <link>http://dev.solita.fi/2025/11/21/mvvm.html</link>
      <pubDate>Fri, 21 Nov 2025 00:00:00 +0000</pubDate>
      <author>open@solita.fi (Solita Oy)</author>
      <guid>http://dev.solita.fi/2025/11/21/mvvm</guid>
	  
      <description>&lt;p&gt;In this blog post we will see how Model-View-ViewModel UI design pattern can be used with different JavaScript, TypeScript, and .NET Core UI frameworks that are used today in many projects.&lt;/p&gt;

&lt;p&gt;MVVM design pattern consists of three connected layers, where topmost is View, that has connection with data binding to ViewModel, which handles Model preparation and needed UI tasks for presenting the data in UI. Models can be data bound to UI like ViewModels. Models are data and domain representations of the actual domain or data models like data entities, or just conceptual models that are connected in example to REST API controllers and/or repositories. In theory, model can contain more information than is stored in the single data entity.&lt;/p&gt;

&lt;p&gt;For the current state of the view for MVVM usage, and available up-to-date frameworks, Microsoft 365 Copilot was used. Copilot provided information about currently available MVVM supporting JavaScript and TypeScript frameworks for UI development. M365 Copilot provided a list of available alternatives for JS and TS MVVM usage, which was compared against already outdated not very well supported Knockout.js framework, that is most pure implementation of MVVM supporting frameworks for JS and TS. Currently available and popular MVVM JS and TS frameworks included Vue.js, Angular, and React, of which React does not really support MVVM design pattern.&lt;/p&gt;

&lt;p&gt;For .NET Core based UI projects, there are two examples for MVVM use from original Windows Presentation Foundation (WPF) MVVM Windows application framework to newer Blazor ASP.NET Core framework, which both can use the same ViewModels and Models, and create different View layer implementations for the different environments, web and desktop app. Both .NET Core UI framework implementations support data binding to ViewModels and Models.&lt;/p&gt;

&lt;h2 id=&quot;model-view-viewmodel-pattern&quot;&gt;Model-View-ViewModel pattern&lt;/h2&gt;

&lt;p&gt;Model-View-ViewModel pattern was introduced for Windows Presentation Foundation (WPF) use in .NET and was also designed in 2005 for HTML use. In this design pattern the UI design is differentiated from program code as XAML or HTML layer, where UI design can be changed without changing the ViewModel or Model code.&lt;/p&gt;

&lt;p&gt;From earlier experience, this kind of design is very useful when developer or designer wants to create different views of the same ViewModel and Models, like product list view, product overlay view, and product details view. All these views can use same ViewModel and Model implementations, and view can be totally different and contain all or part of the commands created for domain logic.&lt;/p&gt;

&lt;p&gt;MVVM pattern uses commands to execute domain commands that are not directly attached to UI. Commands can have parameters, that are used to filter data from ViewModel state and make changes to data storage or do other kind of domain logic functionalities. Command pattern also makes it possible to use Command Query Responsibility Segregation (CQRS) design pattern that differentiates command execution and queries by using concepts like Write model and Read model, which both can reside in different service interfaces. Also, Read model can be seperated data store from write store when using Event-driven architecture like Azure Event Hub event data streaming service.&lt;/p&gt;

&lt;p&gt;MVVM also uses data-binding that can be done with one way, one way to source, or two-way data-binding modes, where all changes in example to product name field, are updated to other places and views that have data-binding to the same Model. Different frameworks support different modes of data-binding.&lt;/p&gt;

&lt;h2 id=&quot;javascript-and-typescript-mvvm-support-in-detail&quot;&gt;JavaScript and TypeScript MVVM support in detail&lt;/h2&gt;

&lt;p&gt;When asking guidance from M365 Copilot, Copilot provided following diagram about MVVM usage for different JS and TS UI frameworks.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../../../img/2025-11-18-mvvm/Designer.png&quot; alt=&quot;MVVM JS and TS frameworks diagram&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Knockout.js and Vue.js provide possibility to create ViewModels, Views, and Models for MVVM style based application. Angular has components model, which is replacing ViewModel, but works similarly.&lt;/p&gt;

&lt;p&gt;Below is a table that was provided by GitHub Copilot, that describes the MVVM support for different UI frameworks.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Framework&lt;/th&gt;
      &lt;th&gt;MVVM Support&lt;/th&gt;
      &lt;th&gt;Data Binding&lt;/th&gt;
      &lt;th&gt;Notes&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Knockout.js&lt;/td&gt;
      &lt;td&gt;Yes&lt;/td&gt;
      &lt;td&gt;Two-way&lt;/td&gt;
      &lt;td&gt;Pure MVVM, less maintained&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Vue.js&lt;/td&gt;
      &lt;td&gt;Partial&lt;/td&gt;
      &lt;td&gt;Two-way&lt;/td&gt;
      &lt;td&gt;Uses ViewModel-like pattern&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Angular&lt;/td&gt;
      &lt;td&gt;Partial&lt;/td&gt;
      &lt;td&gt;One/two-way&lt;/td&gt;
      &lt;td&gt;Uses components, not pure MVVM&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;React&lt;/td&gt;
      &lt;td&gt;No&lt;/td&gt;
      &lt;td&gt;One-way&lt;/td&gt;
      &lt;td&gt;Follows MV* patterns&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Table shows that Knockout.js has been used more in earlier years, is not maintained so well anymore. From second and third options, in example Vue.js is a framework that can be used to create MVVM applications client side in TypeScript or JavaScript with up-to-date framework library.&lt;/p&gt;

&lt;p&gt;Knockout.js, Vue.js and Angular provide possibility to use Two-way data-binding. Because React is not MVVM supporting framework, the usage of React is not the main concern in this blog post.&lt;/p&gt;

&lt;h2 id=&quot;net-core-mvvm-support-in-detail&quot;&gt;.NET Core MVVM support in detail&lt;/h2&gt;

&lt;p&gt;Below GitHub Copilot has created diagram that describes two .NET Core based case examples with WPF Windows application and Blazor Web application that are using MVVM pattern.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../../../img/2025-11-18-mvvm/mermaid-diagram-2025-11-21-141217_c.png&quot; alt=&quot;MVVM Architecture Diagram&quot; /&gt;&lt;/p&gt;

&lt;p&gt;In past projects, there have been successful experiences about creating two user interface projects with same ViewModel library, like it is shown in the above diagram. This means that same business logic, and same UI logic can be used in Windows application and in Web applications with Blazor and WPF.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Framework / UI Type&lt;/th&gt;
      &lt;th&gt;MVVM Support&lt;/th&gt;
      &lt;th&gt;Data Binding&lt;/th&gt;
      &lt;th&gt;Shared ViewModel/Model Support&lt;/th&gt;
      &lt;th&gt;Notes&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;WPF&lt;/td&gt;
      &lt;td&gt;Yes&lt;/td&gt;
      &lt;td&gt;One-way, Two-way&lt;/td&gt;
      &lt;td&gt;Yes&lt;/td&gt;
      &lt;td&gt;Native MVVM, strong XAML binding&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Blazor&lt;/td&gt;
      &lt;td&gt;Yes&lt;/td&gt;
      &lt;td&gt;One-way, Two-way&lt;/td&gt;
      &lt;td&gt;Yes&lt;/td&gt;
      &lt;td&gt;MVVM by convention, supports shared logic&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Both UI frameworks for .NET Core support MVVM design style project fully. In this example case it is also possible to use both for same business case, if native workstation application and web application both are needed, in example in transition phase.&lt;/p&gt;

&lt;h2 id=&quot;experimentation-code-examples&quot;&gt;Experimentation code examples&lt;/h2&gt;

&lt;p&gt;For this blog post M365 Copilot provided code examples for JavaScript UI frameworks, for View, ViewModel, and Model layers, and for different pages in the application.&lt;/p&gt;

&lt;h3 id=&quot;knockoutjs-outdated-pure-mvvm-example&quot;&gt;Knockout.js (Outdated pure MVVM example)&lt;/h3&gt;

&lt;p&gt;Below we can see product UI example code that was created for Knockout.js by M365 Copilot.&lt;/p&gt;

&lt;h4 id=&quot;view-pages-and-single-overlay-view&quot;&gt;View (pages and single overlay view)&lt;/h4&gt;
&lt;div class=&quot;language-html 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;cp&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Knockout MVVM Example&lt;span class=&quot;nt&quot;&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout-min.js&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-bind=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;if: currentView() === 'list'&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Product Catalog&lt;span class=&quot;nt&quot;&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;product-grid&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-bind=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;foreach: products&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;product-card&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;h3&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-bind=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text: name&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Price: &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-bind=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text: price&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Discounted: &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-bind=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text: discountedPrice&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-bind=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;click: $parent.showOverlay&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Quick View&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-bind=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;click: $parent.goToProductPage&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;View Page&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;overlay&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-bind=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;visible: selectedProduct&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;h2&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-bind=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text: selectedProduct().name&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-bind=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text: selectedProduct().description&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Original Price: &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-bind=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text: selectedProduct().price&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Discounted Price: &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-bind=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text: selectedProduct().discountedPrice&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-bind=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;click: closeOverlay&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Close&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-bind=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;if: currentView() === 'product'&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;h1&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-bind=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text: selectedProduct().name&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-bind=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text: selectedProduct().description&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Price: &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-bind=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text: selectedProduct().price&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Discounted: &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-bind=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text: selectedProduct().discountedPrice&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-bind=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;click: goBack&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Back to List&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;app.js&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In this example, there is single page application MVVM View HTML code for Knockout.js application. This code contains 3 different Views Product list page, Product details page, and product overlay page.&lt;/p&gt;

&lt;h4 id=&quot;viewmodel-and-model&quot;&gt;ViewModel and Model&lt;/h4&gt;
&lt;div class=&quot;language-javascript 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;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;price&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;description&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;var&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;price&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ko&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;observable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;price&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;discountedPrice&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ko&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;computed&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;p&quot;&gt;()&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;numericPrice&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;parseFloat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;price&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;replace&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;$&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;dl&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;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;dl&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;nx&quot;&gt;numericPrice&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.9&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toFixed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&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;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ProductListViewModel&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;var&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;products&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ko&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;observableArray&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;nx&quot;&gt;Product&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;Laptop&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;$1200&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;High-end laptop&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;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Product&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;Phone&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;$800&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Latest smartphone&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;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Product&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;Tablet&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;$500&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Portable tablet&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;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;selectedProduct&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ko&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;observable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;currentView&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ko&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;observable&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;list&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;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;showOverlay&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;product&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;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;selectedProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;product&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;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;closeOverlay&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&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;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;selectedProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;null&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;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;goToProductPage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;product&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;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;selectedProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;currentView&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;product&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;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;goBack&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&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;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;currentView&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;list&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;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;selectedProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;null&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;ko&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;applyBindings&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;nx&quot;&gt;ProductListViewModel&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 the above example of the Knockout.js MVVM Product list ViewModel and Product Model, there is M365 Copilot generated version of the design pattern implementation. This example uses same Product Model for all the three views, product list rows, product details page, and for product overlay page for quick views from product list rows to details.&lt;/p&gt;

&lt;h3 id=&quot;vuejs-up-to-date-mvvm-example&quot;&gt;Vue.js (Up to date MVVM example)&lt;/h3&gt;

&lt;p&gt;Below we can see product UI example code that was created for Vue.js by M365 Copilot.&lt;/p&gt;

&lt;h4 id=&quot;view-pages-and-single-overlay-view-1&quot;&gt;View (pages and single overlay view)&lt;/h4&gt;
&lt;div class=&quot;language-html 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;cp&quot;&gt;&amp;lt;!doctype html&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;meta&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;charset=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;utf-8&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Vue Example&lt;span class=&quot;nt&quot;&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;c&quot;&gt;/* minimal styles to keep layout from breaking */&lt;/span&gt;
      &lt;span class=&quot;nc&quot;&gt;.product-grid&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;flex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;gap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;12px&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;nc&quot;&gt;.product-card&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;border&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1px&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;solid&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;#ccc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;12px&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;200px&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;nc&quot;&gt;.overlay&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;fixed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;top&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;20%&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;20%&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;white&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;20px&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;border&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1px&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;solid&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;#333&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;nt&quot;&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- view layer: mount target and router view --&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;app&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;router-view&amp;gt;&amp;lt;/router-view&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Move templates into the HTML view layer --&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;template&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;product-list-template&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Product Catalog&lt;span class=&quot;nt&quot;&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;product-grid&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;product-card&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;v-for=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;p in products&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;:key=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;p.name&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;nt&quot;&gt;&amp;lt;h3&amp;gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Price: $&lt;span class=&quot;nt&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Discounted: $&lt;span class=&quot;nt&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;click=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;showOverlay(p)&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Quick View&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;click=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;goToProductPage(p)&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;View Page&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;v-if=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;selectedProduct&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;overlay&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;lt;h2&amp;gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Original Price: $&lt;span class=&quot;nt&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Discounted Price: $&lt;span class=&quot;nt&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;click=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;closeOverlay&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Close&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;nt&quot;&gt;&amp;lt;template&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;product-page-template&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;h1&amp;gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Price: $&lt;span class=&quot;nt&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Discounted: $&lt;span class=&quot;nt&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;click=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;$router.push('/')&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Back to List&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- include Vue + VueRouter from CDN (or use your local builds) --&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://unpkg.com/vue@3/dist/vue.global.prod.js&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://unpkg.com/vue-router@4/dist/vue-router.global.prod.js&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;./app.js&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In this example, there is single page application MVVM View HTML code for Vue.js application. This code contains 3 different Views Product list page, Product details page, and product overlay page.&lt;/p&gt;

&lt;h4 id=&quot;viewmodel-and-model-1&quot;&gt;ViewModel and Model&lt;/h4&gt;

&lt;div class=&quot;language-javascript 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;// ...existing code...&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Product&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;nx&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;price&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;description&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;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;price&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;description&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;products&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;nx&quot;&gt;Product&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;Laptop&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;mi&quot;&gt;1200&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;High-end laptop&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;Product&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;Phone&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;mi&quot;&gt;800&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;Latest smartphone&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;Product&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;Tablet&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;mi&quot;&gt;500&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;Portable tablet&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;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ProductList&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;na&quot;&gt;template&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;#product-list-template&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;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;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;selectedProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&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;methods&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;showOverlay&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;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;selectedProduct&lt;/span&gt; &lt;span class=&quot;o&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;nx&quot;&gt;closeOverlay&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;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;selectedProduct&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&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;goToProductPage&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;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;$router&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;na&quot;&gt;name&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;product&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;na&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;na&quot;&gt;name&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;nx&quot;&gt;name&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;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ProductPage&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;na&quot;&gt;template&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;#product-page-template&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;na&quot;&gt;computed&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;product&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;nx&quot;&gt;products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;find&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;o&quot;&gt;=&amp;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;nx&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;$route&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;name&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;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;routes&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;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;path&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;/&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;na&quot;&gt;component&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ProductList&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;path&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;/product/:name&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;na&quot;&gt;name&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;product&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;na&quot;&gt;component&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ProductPage&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;router&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;VueRouter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createRouter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;history&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;VueRouter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createWebHashHistory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;routes&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;app&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Vue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createApp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({});&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;use&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;router&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;mount&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;#app&lt;/span&gt;&lt;span class=&quot;dl&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;In the above example of the Vue.js MVVM Product list and ProductPage ViewModels and Product Model, is M365 Copilot generated version of the design pattern implementation. This example uses same Product Model for all the three views, product list rows, product details page, and for product overlay page for quick views from product list rows to details. In this M365 Copilot generated version for Vue.js, the pages have their own dedicated ViewModels, which can reuse the same Model implementation, and could also use same product ViewModel implementation for product specific UI functionalities.&lt;/p&gt;

&lt;h3 id=&quot;react-not-mvvm-example&quot;&gt;React (not MVVM example)&lt;/h3&gt;

&lt;p&gt;Below we can see product UI example code that was created for React by M365 Copilot. This example is not following the MVVM design that was shown for Vue.js and for Knockout.js frameworks before. This example can be used to see differences in the UI implementation between MVVM based design and React design.&lt;/p&gt;

&lt;h4 id=&quot;view-single-page-application-html-code&quot;&gt;View (Single page application HTML code)&lt;/h4&gt;
&lt;div class=&quot;language-html 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;cp&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;title&amp;gt;&lt;/span&gt;React MVVM Example&lt;span class=&quot;nt&quot;&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://unpkg.com/react@18/umd/react.development.js&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://unpkg.com/react-dom@18/umd/react-dom.development.js&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://unpkg.com/react-router-dom@6/umd/react-router-dom.development.js&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;font-family&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Arial&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;sans-serif&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;margin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;20px&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;nc&quot;&gt;.product-grid&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;flex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;gap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;10px&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;flex-wrap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;wrap&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;nc&quot;&gt;.product-card&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;border&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1px&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;solid&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;#ccc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;10px&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;150px&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;nc&quot;&gt;.overlay&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;fixed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;top&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;20%&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;30%&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;#fff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;border&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1px&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;solid&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;#333&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;20px&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;nt&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;margin-top&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;10px&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;nt&quot;&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;root&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;app.js&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text/babel&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://unpkg.com/@babel/standalone/babel.min.js&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In this example, there is single page application View HTML code for React application. In React, this HTML page does not contain all of the MVVM design based views, like Vue.js and Knowckout.js examples did.&lt;/p&gt;

&lt;h4 id=&quot;view-viewmodel-and-model&quot;&gt;View, ViewModel, and Model&lt;/h4&gt;

&lt;div class=&quot;language-javascript 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;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;BrowserRouter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Routes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Route&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useNavigate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useParams&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ReactRouterDOM&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;products&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;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&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;Laptop&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;na&quot;&gt;price&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;description&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;High-end laptop&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;name&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;Phone&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;na&quot;&gt;price&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;800&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;description&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;Latest smartphone&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;name&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;Tablet&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;na&quot;&gt;price&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;500&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;description&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;Portable tablet&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;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ProductList&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;navigate&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useNavigate&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;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;selectedProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;setSelectedProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;React&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;null&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;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;h1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Product&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Catalog&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/h1&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;      &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;product-grid&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&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;products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;map&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;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
          &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;key&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;nx&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;product-card&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;h3&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;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/h3&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;            &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;Price&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;$&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;nx&quot;&gt;price&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/p&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;            &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;Discounted&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;$&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;nx&quot;&gt;price&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.9&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toFixed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/p&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;            &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;onClick&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;nx&quot;&gt;setSelectedProduct&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;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Quick&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;View&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/button&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;            &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;onClick&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;nx&quot;&gt;navigate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`/product/&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;nx&quot;&gt;name&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;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Page&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/button&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;          &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/div&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;        &lt;span class=&quot;p&quot;&gt;))}&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/div&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;      &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;selectedProduct&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;overlay&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;h2&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;selectedProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/h2&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;          &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;p&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;selectedProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/p&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;          &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Original&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Price&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;selectedProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;price&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/p&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;          &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Discounted&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Price&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;selectedProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;price&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.9&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toFixed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/p&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;          &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;onClick&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;nx&quot;&gt;setSelectedProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;null&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;Close&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/button&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;        &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/div&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;      &lt;span class=&quot;p&quot;&gt;)}&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/div&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;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;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ProductPage&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;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useParams&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;navigate&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useNavigate&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;product&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;find&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;o&quot;&gt;=&amp;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;nx&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;name&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;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;h1&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;product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/h1&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;      &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;p&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;product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/p&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;      &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Price&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;price&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/p&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;      &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Discounted&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;price&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.9&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toFixed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/p&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;      &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;onClick&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;nx&quot;&gt;navigate&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;/&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;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Back&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/button&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;    &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/div&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;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;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;App&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;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;BrowserRouter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Routes&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Route&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;element&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;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ProductList&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;sr&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;        &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Route&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/product/:name&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;element&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;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ProductPage&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;sr&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;      &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/Routes&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;    &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/BrowserRouter&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;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;ReactDOM&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createRoot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getElementById&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;root&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;nx&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;App&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In this React application example Product list and Product details pages are separated as own pages with functional design pattern. Overlay product view is included in the product list page function code, and not separated from as its own template here, when M365 Copilot was the one that generated this example. There can be ways to detach the overlay view from the page view, to its own function, but for this blog post, React is not the main concern, because MVVM support is not available in React applications.&lt;/p&gt;

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

&lt;p&gt;When using MVVM design pattern in 2025 it is still possible to use up-to-date UI frameworks for development and create multi-UI applications with same business logic and UI logic layers.&lt;/p&gt;

&lt;p&gt;When separating XAML markup (WPF), and HTML and CSS (JS and TS) there are many possibilities for using already existing UI logic and business logic, and creating updated UI designs, without a need for updating the code that is critical for the business.&lt;/p&gt;

&lt;p&gt;When comparing these three code examples, the MVVM based applications and ViewModels and Models of these examples need no changes when updating the HTML or CSS for the UI, and if UI logic does not change, this MVVM based approach can provide possibility to do UI design changes without JavaScript or TypeScript coding skills.&lt;/p&gt;

&lt;p&gt;Based on experiences from many projects where MVVM design pattern has been used, it has been easy way for maintaining the UI logic and business logic behind the actual UI markup code. Also, different assemblies can be used to extract the ViewModel and Model logic from the View layer in .NET Core project level. This also makes it possible to create new UI View layers for old applications, or creating different View layer projects for different platforms, like MacOS or other operating systems etc.&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>
    
    <item>
      <title>C-ing the web part 2, Lambda boogaloo</title>
      <link>http://dev.solita.fi/2025/11/14/cing-the-web-lambda.html</link>
      <pubDate>Fri, 14 Nov 2025 00:00:00 +0000</pubDate>
      <author>open@solita.fi (Solita Oy)</author>
      <guid>http://dev.solita.fi/2025/11/14/cing-the-web-lambda</guid>
	  
      <description>&lt;p&gt;&lt;a href=&quot;https://dev.solita.fi/2025/03/10/cing-the-web.html&quot;&gt;Last time&lt;/a&gt; we implemented a simple front end
application in C and compiled it to run in the browser using via WebAssembly.&lt;/p&gt;

&lt;p&gt;Now, let’s consider the back end. The traditional way would be to run a C application
in a web server like &lt;a href=&quot;https://httpd.apache.org&quot;&gt;Apache httpd&lt;/a&gt; using the
&lt;a href=&quot;https://en.wikipedia.org/wiki/Common_Gateway_Interface&quot;&gt;Common Gateway Interface&lt;/a&gt;.
    Instead of doing that, let’s run our C code inside AWS Lambda using the
&lt;a href=&quot;https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html&quot;&gt;custom runtime&lt;/a&gt; support.&lt;/p&gt;

&lt;p&gt;With a custom runtime, you can create serverless functions using any language.
The custom runtime bootstrap is based on a simple loop that uses HTTP API to:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;issue a GET request to fetch the next invocation&lt;/li&gt;
  &lt;li&gt;pass it on to the handler for processing&lt;/li&gt;
  &lt;li&gt;issue a POST request to send a response (or an error) back to the client&lt;/li&gt;
  &lt;li&gt;repeat&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To follow along or just try the code yourself, check out the code from the
&lt;a href=&quot;https://github.com/tatut/clambda&quot;&gt;clambda repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;getting-a-proper-image&quot;&gt;Getting a proper image&lt;/h2&gt;

&lt;p&gt;To make a custom runtime, we need to compile our C code in an environment that is compatible with
the AWS Lambda image.&lt;/p&gt;

&lt;p&gt;The easiest way to do this is to just make our own Docker image for building:&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;FROM amazonlinux:2
RUM yum groupinstall -y &quot;Development Tools&quot; &amp;amp;&amp;amp; yum install -y libcurl-devel
ENTRYPOINT [&quot;/opt/clambda/build.sh&quot;]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We start with the Amazon Linux image, install development tools and cURL headers to it.
We will run this image to build both the bootstrap and the handler code. Note that
if you are running on Apple architecture, you should specify
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--platform=linux/amd64&lt;/code&gt; to Docker so that you are building for the correct environment.&lt;/p&gt;

&lt;p&gt;For local testing, we also want to build a runtime image. We start with the provided
image and copy both the bootstrap and compiled handlers to it.&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;FROM public.ecr.aws/lambda/provided:al2023
COPY bootstrap ${LAMBDA_RUNTIME_DIR}
COPY *.o ${LAMBDA_RUNTIME_DIR}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;designing-the-api&quot;&gt;Designing the API&lt;/h2&gt;

&lt;p&gt;We need some way for our bootstrap to hand off requests to the handler, and we want it
to be minimal and feel natural from C.&lt;/p&gt;

&lt;p&gt;I came up with a simple function interface:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-C&quot;&gt;typedef struct Invocation {
  uint8_t *payload; // the JSON payload, NUL terminated
  size_t size; // size of payload, not counting NUL byte

  // File handle to write the response to
  FILE *out;

  void *user_data;
} Invocation;

// optional _init fn to initialize user_data
void *handle_init();

// the handler itself
bool handle(Invocation *inv);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The handler code may provide an initialization function that is named by appending
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_init&lt;/code&gt; to the handler name. The initialization function returns an arbitrary user data
pointer that is passed along in all invocations. This can be used to initialize
any libraries or state needed in the actual invocations. The initialization is only called
once before handling any events.&lt;/p&gt;

&lt;p&gt;The handler itself takes a pointer to an invocation that contains the received
data as a raw pointer and a file pointer for writing output.&lt;/p&gt;

&lt;p&gt;The incoming data is not parsed in any way; it is up to the handler to make sense
of it, like parsing JSON. See next part.&lt;/p&gt;

&lt;p&gt;The output is simpler, the handler doesn’t need to allocate memory buffers, the
bootstrap will provide a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FILE*&lt;/code&gt; handle so the handler can simply call functions
like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fprintf&lt;/code&gt; to write output. Handler can also set the content type.&lt;/p&gt;

&lt;p&gt;Finally, if the handler returns &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;false&lt;/code&gt;, the handler is considered failed and an
error reply is sent.&lt;/p&gt;

&lt;p&gt;The bootstrap will load the handler code via a shared library. The handler only
needs to include the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clambda.h&lt;/code&gt; header file and implement the function.&lt;/p&gt;

&lt;h2 id=&quot;working-with-json&quot;&gt;Working with JSON&lt;/h2&gt;

&lt;p&gt;As Lambda is mainly for receiving and sending JSON, we should provide some
facilities for working with it. C doesn’t really have data structures like objects
built in, and manual memory management makes this more awkward.&lt;/p&gt;

&lt;p&gt;Luckily JSON (as defined by
&lt;a href=&quot;https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf&quot;&gt;ECMA-404&lt;/a&gt;)
is quite a simple format. We can write our own code to extract data from it.
For memory management, especially strings, we can modify and return pointers to the
input buffer directly. Those will be valid for the duration of the Lambda handler.&lt;/p&gt;

&lt;p&gt;We can use some C preprocessor macro magic to make extracting object fields to
C structs a little more convenient. See &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;json.h&lt;/code&gt; file in the repo for more details.
This on-demand extraction obviates the need to parse the JSON data fully into an in-memory
representation. We can just directly grab what we need from it.&lt;/p&gt;

&lt;p&gt;Here’s an example for extracting a JSON 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;nl&quot;&gt;&quot;id&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;line1&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;nl&quot;&gt;&quot;points&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;nl&quot;&gt;&quot;x&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;mf&quot;&gt;42.0&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;y&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;mi&quot;&gt;100&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;nl&quot;&gt;&quot;x&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;mf&quot;&gt;66.2&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;y&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;mi&quot;&gt;90&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;nl&quot;&gt;&quot;x&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;mi&quot;&gt;140&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;y&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;mf&quot;&gt;123.4&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;pre&gt;&lt;code class=&quot;language-C&quot;&gt;typedef struct Point {
  double x, y;
} Point;

typedef struct Line {
  char *id;
  size_t npoints;
  Point *points;
} Line;

bool parse_point(char **at, Point *p) {
  json_object(at, {
    json_field(&quot;x&quot;, json_double, &amp;amp;p-&amp;gt;x);
    json_field(&quot;y&quot;, json_double, &amp;amp;p-&amp;gt;y);
  });
  return true;
}

bool parse_points(char **at, Line *line) {
  json_array(at, &amp;amp;line-&amp;gt;points, &amp;amp;line-&amp;gt;npoints, Point, parse_point);
  return true;
}

bool parse_line(char **at, Line *line) {
 json_object(at, {
    json_field(&quot;id&quot;, json_string_ptr, &amp;amp;line-&amp;gt;id);
    json_field(&quot;points&quot;, parse_points, line);
  });
  return true;
}

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

&lt;p&gt;The above might look a little bit involved when compared to some language that has
convenient generic data structures, but I like that we get the data extracted into
typed structures. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;json_array&lt;/code&gt; macro even handles dynamic size of arrays for us
automatically. The application layer just needs to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;free&lt;/code&gt; any arrays at the end.&lt;/p&gt;

&lt;p&gt;Outputting the response JSON is done with regular C formatted printing.
Utilities for that are left as an exercise for the reader.&lt;/p&gt;

&lt;h2 id=&quot;putting-it-together&quot;&gt;Putting it together&lt;/h2&gt;

&lt;p&gt;We have the API, the bootstrap, and even JSON parsing utilities ready. Let’s make
a simple Lambda that analyzes a line and returns how many points it has and its
length.&lt;/p&gt;

&lt;p&gt;Taking the example from above and adding the handler function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-C&quot;&gt;bool analyze_line(Invocation *inv) {
  char *json = (char*) inv-&amp;gt;payload;
  char **at = &amp;amp;json;
  Line line = {0};
  if(!parse_line(at, &amp;amp;line)) return false;

  printf(&quot;Line %s has %d points\n&quot;, line.id, line.npoints);
  double len = 0;
  if(line.npoints) {
    for(int i=0; i&amp;lt;line.npoints-1; i++) {
      double dx = abs(line.points[i].x - line.points[i+1].x);
      double dy = abs(line.points[i].y - line.points[i+1].y);
      len += sqrt(dx*dx + dy*dy);
    }
  }
  fprintf(inv-&amp;gt;out, &quot;{\&quot;npoints\&quot;: %d, \&quot;length\&quot;: %f}&quot;,
          line.npoints, len);

  free(line.points); // remember to clean up!
  return true;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That’s all there is to it. To test it locally, we can build it by running the following commands:&lt;/p&gt;

&lt;div class=&quot;language-shell 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;nv&quot;&gt;$ &lt;/span&gt;make build
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;./build-handler.sh example
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;make runtime-image
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker run &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; 8080:8080 &lt;span class=&quot;nt&quot;&gt;-t&lt;/span&gt; clambda/runtime example.analyze_line

&lt;span class=&quot;c&quot;&gt;# in another shell, call it:&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;curl http://localhost:8080/2015-03-31/functions/function/invocations &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
       &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{&quot;id&quot;: &quot;viiva&quot;, &quot;points&quot;:[{&quot;x&quot;: 10, &quot;y&quot;: 10}, {&quot;y&quot;:20, &quot;x&quot;: 100}, {&quot;x&quot;: 200, &quot;y&quot;: 222}]}'&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;npoints&quot;&lt;/span&gt;: 3, &lt;span class=&quot;s2&quot;&gt;&quot;length&quot;&lt;/span&gt;: 315.951278&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;See the file &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;example.c&lt;/code&gt; for more examples, including a counter that keeps state between calls.&lt;/p&gt;

&lt;p&gt;For deploying to actual AWS environment, see instructions in the &lt;a href=&quot;https://docs.aws.amazon.com/lambda/latest/dg/runtimes-walkthrough.html&quot;&gt;AWS documentation site&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;caveat-and-final-remarks&quot;&gt;Caveat and final remarks&lt;/h2&gt;

&lt;p&gt;C is a low level language and doesn’t offer memory safety. You should carefully test any code
you are exposing over the internet. There are mitigation strategies like fuzzing (using tools like
&lt;a href=&quot;https://gitlab.com/akihe/radamsa&quot;&gt;radamsa&lt;/a&gt;), using &lt;a href=&quot;https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html#index-fsanitize_003daddress&quot;&gt;AddressSanitizer&lt;/a&gt;
or even compiling with the memory-safe C variant &lt;a href=&quot;https://fil-c.org&quot;&gt;Fil-C&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The upside is that using C can give us small native code that starts up
and executes fast. This makes it a good fit when you have a Lambda that
needs to do some heavier processing.&lt;/p&gt;

&lt;p&gt;Happy hacking!&lt;/p&gt;
</description>
    </item>
    

  </channel>
</rss>
