<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Design Community</title>
    <description>The most recent home feed on Design Community.</description>
    <link>https://design.forem.com</link>
    <atom:link rel="self" type="application/rss+xml" href="https://design.forem.com/feed"/>
    <language>en</language>
    <item>
      <title>Big Tech firms are accelerating AI investments and integration, while regulators and companies focus on safety and responsible adoption.</title>
      <dc:creator>Stelixx Insights</dc:creator>
      <pubDate>Sun, 05 Apr 2026 21:00:48 +0000</pubDate>
      <link>https://design.forem.com/stelixx-insights/big-tech-firms-are-accelerating-ai-investments-and-integration-while-regulators-and-companies-54gc</link>
      <guid>https://design.forem.com/stelixx-insights/big-tech-firms-are-accelerating-ai-investments-and-integration-while-regulators-and-companies-54gc</guid>
      <description>&lt;p&gt;The AI landscape is experiencing unprecedented growth and transformation. This post delves into the key developments shaping the future of artificial intelligence, from massive industry investments to critical safety considerations and integration into core development processes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Areas Explored:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Record-Breaking Investments:&lt;/strong&gt; Major tech firms are committing billions to AI infrastructure, signaling a significant acceleration in the field.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;AI in Software Development:&lt;/strong&gt; We examine how companies are leveraging AI for code generation and the implications for engineering workflows.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Safety and Responsibility:&lt;/strong&gt; The increasing focus on ethical AI development and protecting vulnerable users, particularly minors.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Market Dynamics:&lt;/strong&gt; How AI is influencing stock performance, cloud computing strategies, and global market trends.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Global AI Strategies:&lt;/strong&gt; Companies are adapting AI development for specific regional markets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This deep dive aims to provide developers, tech leaders, and enthusiasts with a comprehensive overview of the current state and future trajectory of AI.&lt;/p&gt;

&lt;h1&gt;
  
  
  AI #ArtificialIntelligence #TechTrends #SoftwareEngineering #MachineLearning #CloudComputing #FutureOfTech #AISafety
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>web3</category>
      <category>blockchain</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Accelerating TURN with eBPF: A Non-Invasive Approach</title>
      <dc:creator>Ivan M</dc:creator>
      <pubDate>Sun, 05 Apr 2026 21:00:11 +0000</pubDate>
      <link>https://design.forem.com/ivan-m-tech/accelerating-turn-with-ebpf-a-non-invasive-approach-ed1</link>
      <guid>https://design.forem.com/ivan-m-tech/accelerating-turn-with-ebpf-a-non-invasive-approach-ed1</guid>
      <description>&lt;p&gt;There is much to be said for the merits of eBPF when it comes to the common problems of network filtering, and one cannot help but observe the swift evolution of this splendid technology. However, one must admit that certain undertakings remain decidedly more tiresome than others. While dropping packets comes across as a rather straightforward affair, the pursuit of accelerating a stateful, high-performance userspace relay server presents a most formidable set of challenges.&lt;/p&gt;

&lt;p&gt;Notably, TURN (&lt;a href="https://datatracker.ietf.org/doc/html/rfc8656#name-detailed-example" rel="noopener noreferrer"&gt;RFC 8656&lt;/a&gt;: Traversal Using Relays around NAT) serves as a proverbial specimen of burning CPU cycles owing to an eye-watering amount of kernel-to-user mode switches and vice versa, yet offloading the channel traffic to the eBPF layer demands a most substantial and, one might say, exhaustive quantity of logic to conduct packet processing with surgical precision.&lt;/p&gt;

&lt;p&gt;The proposition to accelerate TURN using an eBPF bypass scheme has been mooted for a considerable duration. Notably, there have been feature requests (like &lt;a href="https://github.com/coturn/coturn/issues/759#issue-875557133" rel="noopener noreferrer"&gt;this&lt;/a&gt; one in the &lt;code&gt;coturn&lt;/code&gt; project) and solutions, like the exemplary work of Tamás Lévai et al., entitled "&lt;a href="https://dl.acm.org/doi/10.1145/3609021.3609296" rel="noopener noreferrer"&gt;Supercharge WebRTC: Accelerate TURN Services with eBPF/XDP&lt;/a&gt;". Although such remediations are thoughtfully designed to handle the protocol with utmost care, they require substantial assistance from the server to do their job. While this approach is certainly commendable, I take a rather different view on what the more beneficial arrangement might be.&lt;/p&gt;

&lt;p&gt;When it comes to unencrypted TURN traffic, one may construct a stateful eBPF component that is fully protocol-aware but learns about new TURN channel bindings by "snooping", ensuring the userspace server stands unmodified and blissfully unaware of the offload. In this post, I should like to present a humble prototype of this very approach, an open-source project that is named &lt;code&gt;TURN-BPF&lt;/code&gt;.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/ivanmtech" rel="noopener noreferrer"&gt;
        ivanmtech
      &lt;/a&gt; / &lt;a href="https://github.com/ivanmtech/turn-bpf" rel="noopener noreferrer"&gt;
        turn-bpf
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      RFC 8656 channel accelerator
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;TURN-BPF: research into eBPF offloads of RFC 8656 channels&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;
&lt;pre class="notranslate"&gt;&lt;code&gt;TURN-BPF is a personal development effort aiming to reckon the feasibility
of using XDP programs to bypass the userspace for the TURN channel traffic
without the need to tamper with the code of the TURN implementation itself.

These programs conduct NAT (client &amp;lt;&amp;gt; TURN | relay &amp;lt;&amp;gt; peer), strip/add the
TURN channel tag, update the checksums, rewrite the MAC addresses and send
the resulting packets onto the wire via interfaces chosen based on the FIB.

The tool requires no configuration from the user, except for the interface
name(s) and is supposed to snoop on relay allocations and channel bindings
by capturing the said control packet handshakes at the XDP/TC hooks on the
main network interface. For the sake of keeping the channels active in the
userspace TURN server, the tool employs a 'heartbeat' approach, spilling a
small fraction of data packets&lt;/code&gt;&lt;/pre&gt;…&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/ivanmtech/turn-bpf" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        [ CLIENTS ]                                 [ PEERS ]
          |     |                                       |
   (STUN) |     | (Tagged Data)                  (Untagged Data)
          |     |                                       |
 +--------+-----+--------+                     +--------+--------+
 | Interface for Clients |                     | Relay Interface |
 +--------+-----+--------+                     +--------+--------+
          |     |                                       |
          |     +-----+                                 |
          |           |                                 |
 =========|===========|=================================|=========
 KERNEL   |           |                                 |
          |           |                                 |
 +--------+--------+  |   +-------------------------+   |
 | XDP/TC Snoopers |  +--&amp;lt;|&amp;gt; XDP cli2rem / rem2cli &amp;lt;|&amp;gt;--+
 +--------+--------+      +---+---------------------+   |
   (Maps) |                   |   (Fast Path)           |
          |                   |                         ^
          |           (Heartbeat Spill)                 |
          |                   |                         |
 =========|===================|=========================|=========
 USERLAND |                   |                         |
          |                   +----&amp;gt;+-------------+--&amp;gt;--+
          |                         | TURN Server |
          +------------------------&amp;lt;+&amp;gt;------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Architecture: The Snooping Approach
&lt;/h2&gt;

&lt;p&gt;The structural underpinnings of the arrangement include a set of XDP and TC (Traffic Control) programs written in C, some of which snoop on control packets to commit the channels for an offload, while others conduct the offload per se. Depending on the configuration (a single network interface acting as both the client endpoint and the relay versus a separate client-facing interface and one or multiple relays), the eBPF component provides either a separate XDP &lt;code&gt;rem2cli&lt;/code&gt; program or a combined XDP section with &lt;code&gt;cli2rem&lt;/code&gt;, &lt;code&gt;rem2cli&lt;/code&gt;, and the STUN snooper baked in.&lt;/p&gt;

&lt;p&gt;One might inquire why the tool employs a TC hook for snooping on the server's responses rather than keeping everything within the XDP layer. The reasoning is as follows: while XDP is unparalleled for raw speed on ingress, the TC egress hook on the client-facing interface allows one to observe the packets after they have been processed by the userspace stack and the kernel's networking subsystem. At this stage, the ChannelBind success response is fully formed and ready to depart. By intercepting it here, one can ensure that a channel is only committed to the fast path once the server has officially sanctioned the allocation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the Hood: FIB Lookups &amp;amp; Heartbeats
&lt;/h2&gt;

&lt;p&gt;Indubitably, the implementation possesses considerably more depth than might initially meet the eye. In particular, the knowledge of which IP addresses map to which MAC addresses and, more crucially, network interfaces, comes from the FIB lookup. The lookup is performed in the control path (in the snoopers), when the channel binding is committed, and relies upon a remarkably helpful and powerful building block from the kernel, the &lt;code&gt;bpf_fib_lookup&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;Furthermore, to ensure the longevity of the session within the userspace daemon, the tool employs a "heartbeat spill" mechanism. By deliberately passing a minute fraction of the data packets up the network stack, we allow the TURN server to perceive the channel as active, thereby preventing the premature expiration of the allocation while the bulk of the throughput enjoys the celerity of the eBPF fast path. Regrettably, there are certain limitations, too. The project, which is merely a proof-of-concept, does not handle encrypted channels and can only support IPv4 traffic.&lt;/p&gt;

&lt;p&gt;The tool comes with a loader program written in Rust, which blocks waiting for the &lt;code&gt;Ctrl+C&lt;/code&gt; keystroke upon successful activation of the kernel component. This part is a hodgepodge of foundational knowledge from the textbook I have been reading, some AI advice, and my occupational hazards from the ten years of experience in C programming; therefore, one should take the code quality with a grain of salt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment &amp;amp; Verification
&lt;/h2&gt;

&lt;p&gt;On Debian 13 (kernel 6.12), one may run the tool as follows (which should be done in a separate terminal on the server machine, prior to the launch of TURN):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--yes&lt;/span&gt; cargo clang git libelf-dev pkg-config
git clone https://github.com/ivanmtech/turn-bpf
&lt;span class="nb"&gt;cd &lt;/span&gt;turn-bpf
cargo build
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./target/debug/turn-bpf &amp;lt;main_ifname&amp;gt; &lt;span class="o"&gt;[&lt;/span&gt;relay_ifnames...]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My poor man's test rig consisted of a pair of laptops connected via a commodity 100 Megabit/s USB Ethernet link. The server-side &lt;code&gt;coturn&lt;/code&gt; configuration was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;listening-port&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3478&lt;/span&gt;
&lt;span class="py"&gt;listening-ip&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;192.168.47.1&lt;/span&gt;
&lt;span class="py"&gt;relay-ip&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;192.168.47.1&lt;/span&gt;
&lt;span class="py"&gt;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;user:password&lt;/span&gt;
&lt;span class="py"&gt;realm&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;turn.test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are my configuration steps on the server-side laptop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--yes&lt;/span&gt; coturn
&lt;span class="nb"&gt;sudo &lt;/span&gt;service coturn stop
&lt;span class="nb"&gt;sudo &lt;/span&gt;nmcli device &lt;span class="nb"&gt;set &lt;/span&gt;enx0 managed no
&lt;span class="nb"&gt;sudo &lt;/span&gt;ip addr add 192.168.47.1/24 dev enx0
&lt;span class="nb"&gt;sudo &lt;/span&gt;ip &lt;span class="nb"&gt;link set &lt;/span&gt;enx0 up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the client-side laptop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nmcli device &lt;span class="nb"&gt;set &lt;/span&gt;enx0 managed no
&lt;span class="nb"&gt;sudo &lt;/span&gt;ip addr add 192.168.47.2/24 dev enx0
&lt;span class="nb"&gt;sudo &lt;/span&gt;ip &lt;span class="nb"&gt;link set &lt;/span&gt;enx0 up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running &lt;code&gt;coturn&lt;/code&gt; on the server-side laptop is as simple as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;turnserver &lt;span class="nt"&gt;-c&lt;/span&gt; ~/test.cfg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While the client-side laptop acts both as a client and a remote peer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;turnutils_peer &lt;span class="nt"&gt;-L&lt;/span&gt; 192.168.47.2
turnutils_uclient &lt;span class="nt"&gt;-u&lt;/span&gt; user password &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;-w&lt;/span&gt; p &lt;span class="nt"&gt;-e&lt;/span&gt; 192.168.47.2 &lt;span class="nt"&gt;-n&lt;/span&gt; 100000 &lt;span class="nt"&gt;-m&lt;/span&gt; 50 &lt;span class="nt"&gt;-g&lt;/span&gt; 192.168.47.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Performance Results
&lt;/h2&gt;

&lt;p&gt;In my case, running &lt;code&gt;pidstat&lt;/code&gt; for &lt;code&gt;coturn&lt;/code&gt; indicates CPU usage at a negligible level, virtually 0%, which jumps swiftly to 20% when &lt;code&gt;Ctrl+C&lt;/code&gt; is pressed in the &lt;code&gt;TURN-BPF&lt;/code&gt; terminal. System-wide CPU usage is 0-1% when the offload is active, versus 6-7% in its absence. Again, my test rig was lacking in many ways, so the results presented might not necessarily meet production-grade expectations.&lt;/p&gt;

&lt;p&gt;As I continue to explore the frontiers of high-performance networking, I should like to remain open to communication with peers and would be delighted to converse about any other architectural and system design challenges of the modern age.&lt;/p&gt;

</description>
      <category>ebpf</category>
      <category>performance</category>
      <category>showdev</category>
      <category>systemsprogramming</category>
    </item>
    <item>
      <title>How to Set Up Qodo AI in VS Code: Installation Guide</title>
      <dc:creator>Rahul Singh</dc:creator>
      <pubDate>Sun, 05 Apr 2026 21:00:00 +0000</pubDate>
      <link>https://design.forem.com/rahulxsingh/how-to-set-up-qodo-ai-in-vs-code-installation-guide-2983</link>
      <guid>https://design.forem.com/rahulxsingh/how-to-set-up-qodo-ai-in-vs-code-installation-guide-2983</guid>
      <description>&lt;h2&gt;
  
  
  Why set up Qodo in VS Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Getting AI-powered code review and test generation directly in your editor eliminates context switching and catches issues before code ever reaches a pull request.&lt;/strong&gt; Most developers spend their day inside VS Code, and adding Qodo to that workflow means you can generate unit tests, get code suggestions, and review your own code without opening a browser or waiting for a CI pipeline to run.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/tool/qodo/"&gt;Qodo&lt;/a&gt; - formerly known as CodiumAI - is an AI code quality platform that combines test generation with code review. While many teams know Qodo for its &lt;a href="https://dev.to/blog/qodo-review/"&gt;PR review capabilities&lt;/a&gt;, the VS Code extension brings those same AI capabilities into your local development environment. You can generate comprehensive unit tests for any function with a single command, get real-time code suggestions, and chat with an AI assistant that understands your codebase context.&lt;/p&gt;

&lt;p&gt;The VS Code extension is the fastest way to start using Qodo. Installation takes under five minutes, the free Developer plan gives you 250 credits per month at no cost, and you do not need to configure any CI/CD pipelines or Git integrations to start generating tests and reviewing code locally.&lt;/p&gt;

&lt;p&gt;This guide walks through every step - from installing the extension to generating your first tests to configuring advanced settings for your workflow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fid5jvbatb7drwkk5ns2g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fid5jvbatb7drwkk5ns2g.png" alt="Qodo screenshot" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before you begin, confirm you have the following ready:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visual Studio Code&lt;/strong&gt; version 1.80 or later installed on your machine (macOS, Windows, or Linux)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An active internet connection&lt;/strong&gt; for the initial installation and for AI-powered features that rely on cloud-hosted models&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A GitHub, Google, or email account&lt;/strong&gt; for signing in to Qodo (no enterprise subscription required)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A project with source code&lt;/strong&gt; open in VS Code to test Qodo's features after installation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No API keys, Docker installations, or terminal commands are needed. The entire setup happens within VS Code's graphical interface. If you have used the older CodiumAI extension before, the &lt;a href="https://dev.to/blog/codiumai-to-qodo/"&gt;rebrand to Qodo&lt;/a&gt; means you should update to the latest Qodo-branded extension for continued support and new features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 - Install the Qodo extension
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Qodo extension is available directly from the VS Code Marketplace.&lt;/strong&gt; Here is how to find and install it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open VS Code&lt;/li&gt;
&lt;li&gt;Press &lt;code&gt;Ctrl+Shift+X&lt;/code&gt; on Windows/Linux or &lt;code&gt;Cmd+Shift+X&lt;/code&gt; on macOS to open the Extensions view&lt;/li&gt;
&lt;li&gt;Type &lt;strong&gt;"Qodo"&lt;/strong&gt; in the search bar at the top of the Extensions panel&lt;/li&gt;
&lt;li&gt;Locate the extension published by &lt;strong&gt;Qodo&lt;/strong&gt; (you may also see it listed with the subtitle "formerly CodiumAI")&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;Install&lt;/strong&gt; button&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The installation is typically complete within 10 to 20 seconds depending on your connection speed. After installation, you will see the Qodo icon appear in the Activity Bar on the left side of VS Code. This icon opens the Qodo panel where you interact with all of the extension's features.&lt;/p&gt;

&lt;p&gt;If you previously had the CodiumAI extension installed, VS Code may have already migrated it to the Qodo-branded version through an automatic update. Check your installed extensions list to verify you are running the latest version. If you see both a CodiumAI and a Qodo extension, uninstall the CodiumAI one and keep the Qodo extension.&lt;/p&gt;

&lt;p&gt;You can also install the extension from the command line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;code &lt;span class="nt"&gt;--install-extension&lt;/span&gt; Qodo.qodo-vscode
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2 - Sign in to your Qodo account
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;After installation, you need to sign in to activate the extension.&lt;/strong&gt; Qodo requires authentication to manage your credits and connect your activity to your account.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click the &lt;strong&gt;Qodo icon&lt;/strong&gt; in the Activity Bar on the left side of VS Code&lt;/li&gt;
&lt;li&gt;The Qodo panel opens with a sign-in prompt&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Sign In&lt;/strong&gt; and choose your preferred authentication method:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt; - recommended if you plan to use Qodo's PR review features later&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google&lt;/strong&gt; - quick sign-in with your Google account&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email&lt;/strong&gt; - create a standalone Qodo account with any email address&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;A browser window opens for authentication - complete the sign-in process&lt;/li&gt;
&lt;li&gt;Return to VS Code where the Qodo panel now shows your account information and remaining credits&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The free Developer plan activates automatically when you create an account. You get 250 credits per calendar month for IDE and CLI interactions, plus 30 PR reviews per month if you later connect Qodo to your Git repositories. No credit card is required.&lt;/p&gt;

&lt;p&gt;After signing in, the Qodo panel displays your current credit balance, the AI model in use, and quick access to the extension's main features: test generation, code chat, and code review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 - Generate your first tests
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Test generation is Qodo's signature feature and the best way to verify that the extension is working correctly.&lt;/strong&gt; Here is how to generate your first batch of unit tests:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open any source file in your project that contains at least one function or method&lt;/li&gt;
&lt;li&gt;Place your cursor inside a function you want to test, or select the entire function&lt;/li&gt;
&lt;li&gt;Open the Qodo chat panel and type &lt;code&gt;/test&lt;/code&gt;, or right-click in the editor and select the Qodo test generation option from the context menu&lt;/li&gt;
&lt;li&gt;Qodo analyzes the function's behavior, input types, conditional branches, and error paths&lt;/li&gt;
&lt;li&gt;Within a few seconds, Qodo generates a complete set of unit tests&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The generated tests include coverage for multiple scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Happy path&lt;/strong&gt; - the function works as expected with valid inputs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge cases&lt;/strong&gt; - null values, empty strings, boundary numbers, and empty arrays&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error scenarios&lt;/strong&gt; - invalid inputs, missing parameters, and exception handling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type variations&lt;/strong&gt; - different input types that the function might receive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Qodo detects your project's existing testing framework and generates tests accordingly. If your project uses pytest, you get pytest-style tests. If it uses Jest, you get Jest tests. Supported frameworks include pytest, unittest, Jest, Vitest, Mocha, JUnit 4, JUnit 5, Go's testing package, NUnit, xUnit, and RSpec.&lt;/p&gt;

&lt;p&gt;Here is an example of what Qodo might generate for a simple utility function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Your source code
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;percentage&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Price cannot be negative&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;percentage&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;percentage&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Percentage must be between 0 and 100&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;percentage&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Qodo-generated tests
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;your_module&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;calculate_discount&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_calculate_discount_standard&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;calculate_discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;90.0&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_calculate_discount_zero_percentage&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;calculate_discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;100.0&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_calculate_discount_full_percentage&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;calculate_discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_calculate_discount_negative_price&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Price cannot be negative&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;calculate_discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_calculate_discount_percentage_over_100&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Percentage must be between 0 and 100&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;calculate_discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;110&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_calculate_discount_negative_percentage&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Percentage must be between 0 and 100&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;calculate_discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each test generation request consumes 1 credit from your monthly balance. With 250 free credits per month, you can generate tests for roughly 250 functions before needing a paid plan. For more details on how Qodo approaches test generation across different languages and frameworks, see our deep dive on &lt;a href="https://dev.to/blog/qodo-test-generation/"&gt;Qodo test generation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 - Explore code suggestions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Beyond test generation, Qodo provides an interactive chat interface for code review, explanation, and refactoring.&lt;/strong&gt; The chat panel in the Qodo sidebar lets you ask questions about your code and get AI-powered responses.&lt;/p&gt;

&lt;p&gt;Here are the key commands you can use in the Qodo chat panel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/test&lt;/code&gt; - generate unit tests for the selected function&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/review&lt;/code&gt; - get a code review of the selected code with suggestions for improvements&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/explain&lt;/code&gt; - get a detailed explanation of what the selected code does&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/improve&lt;/code&gt; - get refactoring suggestions for the selected code&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/docstring&lt;/code&gt; - generate documentation for the selected function&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To use any command, select the code you want to analyze in the editor and then type the command in the Qodo chat panel. You can also ask free-form questions about your code by typing natural language queries directly into the chat.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;/review&lt;/code&gt; command is particularly useful for self-review before opening a pull request. It analyzes your code for potential bugs, security issues, performance anti-patterns, and readability improvements - similar to what &lt;a href="https://dev.to/blog/qodo-review/"&gt;Qodo's PR review&lt;/a&gt; does on pull requests, but available locally before you push your code.&lt;/p&gt;

&lt;p&gt;Each chat interaction consumes credits based on the AI model you are using. Standard models consume 1 credit per request, while premium models like Claude Opus consume 5 credits per request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 - Configure settings
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Customizing Qodo's settings lets you tailor the extension to your workflow and preferences.&lt;/strong&gt; Open VS Code Settings with &lt;code&gt;Ctrl+,&lt;/code&gt; (or &lt;code&gt;Cmd+,&lt;/code&gt; on macOS) and search for "Qodo" to see all available configuration options.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key settings to configure
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Default AI model&lt;/strong&gt; - Choose the AI model that powers Qodo's responses. Options include GPT-4o (balanced speed and quality), Claude 3.5 Sonnet (strong reasoning), and DeepSeek-R1. Premium models provide higher-quality output but consume more credits per request. Start with the default model and switch to a premium model only when you need deeper analysis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test generation preferences&lt;/strong&gt; - Configure how Qodo generates tests, including the target testing framework, test file naming conventions, and whether to include docstrings in generated tests. If Qodo is not detecting your testing framework correctly, you can set it explicitly in the settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local LLM support&lt;/strong&gt; - If your organization requires that code never leaves your machine, enable Local LLM mode through Ollama. This routes all AI processing through a locally hosted model instead of Qodo's cloud API. Set this up by installing Ollama on your machine, downloading a supported model, and pointing Qodo to your local Ollama endpoint in the extension settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Telemetry&lt;/strong&gt; - Control whether the extension sends usage data to Qodo. You can disable telemetry entirely from the settings panel if your organization's policies require it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keyboard shortcuts
&lt;/h3&gt;

&lt;p&gt;Set up keyboard shortcuts for the commands you use most frequently. Open the Keyboard Shortcuts editor with &lt;code&gt;Ctrl+K Ctrl+S&lt;/code&gt; (or &lt;code&gt;Cmd+K Cmd+S&lt;/code&gt; on macOS) and search for "Qodo" to see all available commands. Common shortcuts to configure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate tests for the selected function&lt;/li&gt;
&lt;li&gt;Open the Qodo chat panel&lt;/li&gt;
&lt;li&gt;Run a code review on the current file&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tips for getting the most out of Qodo in VS Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Follow these practices to maximize the quality of Qodo's output.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write clear function signatures.&lt;/strong&gt; Qodo produces better tests and suggestions when it can understand your function's input types and return values. Use type hints in Python, TypeScript annotations, or JSDoc comments in JavaScript. A function with &lt;code&gt;def process_order(order: Order, discount: float) -&amp;gt; Receipt&lt;/code&gt; gives Qodo far more context than &lt;code&gt;def process_order(order, discount)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep functions focused.&lt;/strong&gt; Functions that do one thing well receive higher-quality test generation than monolithic functions with multiple responsibilities. If Qodo's generated tests seem shallow or miss important scenarios, consider breaking the function into smaller, more testable units.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review generated tests before committing.&lt;/strong&gt; Qodo's tests are a strong starting point, but they are not a substitute for human judgment. Review each generated test for correctness, especially around mocking complex dependencies, domain-specific assertions, and integration with external services. Treat generated tests as a draft that saves you 20 to 30 minutes of setup work per function.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use the /review command before opening PRs.&lt;/strong&gt; Running a local code review catches issues that you can fix immediately, reducing the back-and-forth in pull request reviews. This is especially valuable for catching security issues, missing error handling, and logic errors before they reach your team's review queue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Switch models based on the task.&lt;/strong&gt; Use the standard model for quick test generation and simple questions. Switch to a premium model when you need deeper analysis of complex business logic or want more thorough code review feedback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If you encounter issues with the Qodo extension, work through these common problems.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Extension not appearing after installation.&lt;/strong&gt; Restart VS Code after installing the extension. If the Qodo icon still does not appear in the Activity Bar, check that the extension is enabled in the Extensions view. Some organizations use VS Code policies that restrict extension installations - check with your IT team if the extension appears grayed out or disabled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sign-in failing or timing out.&lt;/strong&gt; Ensure your browser is not blocking popups from Qodo's authentication service. Try signing in with a different authentication method (GitHub instead of Google, or vice versa). If you are behind a corporate proxy or VPN, the proxy may be blocking requests to Qodo's API endpoints - check with your network administrator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No credits remaining.&lt;/strong&gt; The free Developer plan provides 250 credits per month, and credits reset every 30 days from the first message you sent, not on a calendar schedule. Check your remaining balance in the Qodo panel. If you consistently run out of credits, the Teams plan at $30/user/month provides 2,500 credits per user per month. See our full &lt;a href="https://dev.to/blog/qodo-pricing/"&gt;Qodo pricing&lt;/a&gt; breakdown for a detailed comparison of all plan tiers and what each includes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test generation producing low-quality output.&lt;/strong&gt; Ensure you are selecting a complete function rather than a partial code block. Add type annotations and docstrings to give Qodo more context. Try a premium AI model for more thorough test generation. For complex functions with deep dependency chains, you may need to refine the generated tests manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Extension consuming too many resources.&lt;/strong&gt; If VS Code feels sluggish after installing Qodo, check the extension's memory usage in the VS Code Process Explorer (Help then Process Explorer). Disable features you do not use, such as inline suggestions, to reduce resource consumption. Updating to the latest extension version often resolves performance issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conflict with other extensions.&lt;/strong&gt; Qodo generally works well alongside other AI extensions including GitHub Copilot. If you experience conflicts, try disabling other AI extensions temporarily to isolate the issue. Qodo and Copilot serve different purposes and should not conflict - Copilot handles inline completions while Qodo handles test generation and code review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alternatives to Qodo for VS Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If Qodo does not fit your workflow, several alternatives provide AI-powered capabilities in VS Code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/tool/codeant-ai/"&gt;CodeAnt AI&lt;/a&gt;&lt;/strong&gt; ($24-40/user/month) combines AI code review with SAST security scanning, secret detection, and DORA metrics in a single platform. It supports 30+ languages and all four major Git platforms. CodeAnt AI focuses on code health at the organizational level rather than individual IDE interactions, making it a strong choice for teams that want PR review, security scanning, and engineering metrics bundled together. See our full &lt;a href="https://dev.to/blog/qodo-alternatives/"&gt;Qodo alternatives&lt;/a&gt; comparison for a broader landscape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Copilot&lt;/strong&gt; ($10-39/user/month) is the most widely adopted AI coding assistant. It excels at inline code completions and has a chat interface for code explanations and generation. Copilot's test generation is less specialized than Qodo's but covers a wider range of coding tasks. Many developers use both Copilot and Qodo side by side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tabnine&lt;/strong&gt; ($9/user/month and up) offers AI code completions with a strong focus on privacy and self-hosted deployment. It is a good alternative for teams that need code completion without sending code to external servers but does not offer Qodo's depth of test generation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sourcery&lt;/strong&gt; ($10/user/month and up) specializes in Python code quality with automated refactoring suggestions. It is more limited in language support than Qodo but provides highly targeted feedback for Python teams.&lt;/p&gt;

&lt;p&gt;For a comprehensive comparison of all available options, see our full guide to &lt;a href="https://dev.to/blog/qodo-alternatives/"&gt;Qodo alternatives&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Setting up Qodo in VS Code takes less than five minutes and immediately gives you access to AI-powered test generation and code review inside your editor.&lt;/strong&gt; The free Developer plan with 250 monthly credits is enough for most individual developers to evaluate whether Qodo's approach to test generation fits their workflow.&lt;/p&gt;

&lt;p&gt;The key steps are straightforward: install the extension from the marketplace, sign in with GitHub, Google, or email, and start generating tests with the &lt;code&gt;/test&lt;/code&gt; command. From there, explore the &lt;code&gt;/review&lt;/code&gt; and &lt;code&gt;/improve&lt;/code&gt; commands for code quality feedback, configure your preferred AI model, and set up keyboard shortcuts for the commands you use most.&lt;/p&gt;

&lt;p&gt;For teams that want to extend Qodo beyond the IDE into pull request workflows, the natural next step is connecting Qodo to your Git repositories for automated PR review. Our guides on &lt;a href="https://dev.to/blog/qodo-review/"&gt;Qodo PR review&lt;/a&gt; and &lt;a href="https://dev.to/blog/qodo-test-generation/"&gt;Qodo test generation&lt;/a&gt; cover those workflows in detail. If you are curious about how Qodo's pricing compares to other tools in the market, our &lt;a href="https://dev.to/blog/qodo-alternatives/"&gt;Qodo alternatives&lt;/a&gt; breakdown provides current pricing and feature comparisons across ten competing platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/qodo-jetbrains-setup/"&gt;How to Set Up Qodo AI in JetBrains (IntelliJ, PyCharm, WebStorm)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/qodo-merge-github/"&gt;Qodo Merge GitHub Integration: Automated PR Review Setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/best-ai-code-review-tools/"&gt;Best AI Code Review Tools in 2026 - Expert Picks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/best-ai-pr-review-tools/"&gt;Best AI Code Review Tools for Pull Requests in 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/best-ai-test-generation-tools/"&gt;Best AI Test Generation Tools in 2026: Complete Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h3&gt;
  
  
  How do I install Qodo AI in VS Code?
&lt;/h3&gt;

&lt;p&gt;Open VS Code and press Ctrl+Shift+X (or Cmd+Shift+X on macOS) to open the Extensions view. Search for 'Qodo' in the marketplace search bar. Click Install on the Qodo extension published by Qodo (formerly CodiumAI). After installation, the Qodo icon appears in the Activity Bar on the left side of VS Code. Click it to open the Qodo panel and sign in to start using AI-powered code review and test generation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the Qodo VS Code extension free?
&lt;/h3&gt;

&lt;p&gt;Yes. The Qodo VS Code extension is free to install. Qodo offers a free Developer plan that includes 250 credits per calendar month for IDE interactions and 30 PR reviews per month. Most standard operations consume 1 credit each. The free tier is sufficient for individual developers to evaluate test generation and code suggestions. The paid Teams plan at $30/user/month increases credits to 2,500 per user per month.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between Qodo and CodiumAI in the VS Code marketplace?
&lt;/h3&gt;

&lt;p&gt;Qodo is the new name for CodiumAI. The company rebranded from CodiumAI to Qodo in 2024. The VS Code extension was updated to reflect the new branding. If you search for CodiumAI in the marketplace, you will find the Qodo extension since the old listing redirects to the new one. There is no separate CodiumAI extension anymore. For more details on the rebrand, see our article on the CodiumAI to Qodo transition.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Qodo work with VS Code on macOS, Windows, and Linux?
&lt;/h3&gt;

&lt;p&gt;Yes. The Qodo VS Code extension works on all platforms where VS Code runs, including macOS, Windows, and Linux. The extension itself runs within VS Code and communicates with Qodo's cloud API, so there are no platform-specific dependencies or installation requirements. The experience is identical across all operating systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use Qodo in VS Code without an internet connection?
&lt;/h3&gt;

&lt;p&gt;Limited functionality is available offline. The Qodo extension requires an internet connection for AI-powered features like test generation and code suggestions, since these rely on cloud-hosted large language models. However, Qodo supports Local LLM mode through Ollama, which allows you to run models entirely on your machine without sending code to external servers. This is useful for air-gapped environments and teams with strict data privacy requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I generate tests with Qodo in VS Code?
&lt;/h3&gt;

&lt;p&gt;Select a function in your editor, then use the /test command in the Qodo chat panel or right-click and choose the Qodo test generation option from the context menu. Qodo analyzes the function's behavior, input types, and conditional branches, then generates complete unit tests covering the happy path, edge cases, and error scenarios. Tests are generated in your project's existing testing framework such as pytest, Jest, JUnit, or Vitest.&lt;/p&gt;

&lt;h3&gt;
  
  
  What AI models does Qodo support in VS Code?
&lt;/h3&gt;

&lt;p&gt;Qodo supports multiple AI models in its VS Code extension including GPT-4o, Claude 3.5 Sonnet, and DeepSeek-R1. Premium models like Claude Opus consume more credits per request (5 credits) compared to standard models (1 credit). You can switch between models in the Qodo settings panel within VS Code. Local LLM support through Ollama is also available for teams that need to keep all code processing on their own machines.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is Qodo not showing suggestions in VS Code?
&lt;/h3&gt;

&lt;p&gt;Check these common causes: you may not be signed in to the Qodo extension, your monthly credit balance may be exhausted, the extension may need an update, or your internet connection may be interrupted. Open the Qodo panel in VS Code and verify your sign-in status and remaining credits. Also check the VS Code Output panel (View then Output then select Qodo from the dropdown) for error messages. Restarting VS Code often resolves temporary connection issues.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use Qodo alongside GitHub Copilot in VS Code?
&lt;/h3&gt;

&lt;p&gt;Yes. Qodo and GitHub Copilot serve different purposes and can coexist in VS Code without conflicts. Copilot provides inline code completions while you type, while Qodo focuses on test generation, code review, and chat-based assistance. Many developers use both extensions simultaneously - Copilot for writing code and Qodo for generating tests and reviewing code quality. There are no known extension conflicts between the two.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I update the Qodo extension in VS Code?
&lt;/h3&gt;

&lt;p&gt;VS Code updates extensions automatically by default. To manually check for updates, open the Extensions view (Ctrl+Shift+X or Cmd+Shift+X), click the three-dot menu at the top of the Extensions panel, and select Check for Extension Updates. If an update is available for Qodo, click the Update button. It is recommended to keep the extension updated to access the latest AI models, bug fixes, and feature improvements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Qodo in VS Code support all programming languages?
&lt;/h3&gt;

&lt;p&gt;Qodo supports all major programming languages in VS Code including JavaScript, TypeScript, Python, Java, Go, C++, C#, Ruby, PHP, Kotlin, and Rust. The AI engine uses large language models for semantic understanding, so it can handle virtually any language. Test generation quality is strongest for languages with mature testing ecosystems like Python (pytest), JavaScript (Jest), Java (JUnit), and TypeScript (Vitest).&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I configure Qodo settings in VS Code?
&lt;/h3&gt;

&lt;p&gt;Open VS Code Settings (Ctrl+Comma or Cmd+Comma on macOS) and search for 'Qodo' to see all available configuration options. You can also access settings through the Qodo panel in the Activity Bar. Key settings include the default AI model, test generation preferences, inline suggestion behavior, and telemetry options. Changes take effect immediately without restarting VS Code.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://aicodereview.cc/blog/qodo-vscode-setup/" rel="noopener noreferrer"&gt;aicodereview.cc&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>codereview</category>
      <category>ai</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>VoiceScribe</title>
      <dc:creator>Jan Klein</dc:creator>
      <pubDate>Sun, 05 Apr 2026 20:40:47 +0000</pubDate>
      <link>https://design.forem.com/jan-klein/voicescribe-4k99</link>
      <guid>https://design.forem.com/jan-klein/voicescribe-4k99</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5sd3f4jp1q1z5xfjpmad.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5sd3f4jp1q1z5xfjpmad.png" alt="VoiceScribe" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  VoiceScribe
&lt;/h2&gt;

&lt;h3&gt;
  
  
  RealTime Speech To Text App Built with Google AI Studio
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Live app: &lt;a href="https://voice-scribe.netlify.app" rel="noopener noreferrer"&gt;voice-scribe.netlify.app&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What it does
&lt;/h3&gt;

&lt;p&gt;VoiceScribe is a realtime speech to text web app that supports &lt;strong&gt;20 languages&lt;/strong&gt;. You speak, it writes. Then you copy or share the text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It works on all browsers&lt;/strong&gt; Chrome, Firefox, Safari, Edge, and mobile browsers.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;p&gt;Simple. Your browser captures your voice. Google's AI turns it into text. You see it instantly.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I built it
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;HTML&lt;/strong&gt; structure of the page, language dropdown, buttons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CSS&lt;/strong&gt; styling, responsive design, works on phone and desktop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vanilla JavaScript&lt;/strong&gt; microphone access, sending audio to Google API, displaying text, copy and share functions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google AI Studio&lt;/strong&gt; provides the API key for Google Cloud Speech-to-Text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Netlify&lt;/strong&gt; hosting, free.&lt;/p&gt;

&lt;p&gt;No frameworks. No backend. No database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Useful for education
&lt;/h3&gt;

&lt;p&gt;This app is a perfect teaching example for students who want to learn:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How browser APIs work (microphone, clipboard, sharing)&lt;/li&gt;
&lt;li&gt;How to integrate Google AI into a real project&lt;/li&gt;
&lt;li&gt;How to build a complete useful app with just HTML, CSS, and JavaScript&lt;/li&gt;
&lt;li&gt;How to handle permissions, errors, and different browsers&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  My experience with Google AI Studio
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;It helped me save time with API access. But it has problems.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It does not follow human language instructions well. I had to use &lt;strong&gt;custom instructions&lt;/strong&gt; which only developers know how to write.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It sometimes adds code without asking or makes failures. You should &lt;strong&gt;make a backup each time you create a new version&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Try it
&lt;/h3&gt;

&lt;p&gt;Open &lt;strong&gt;&lt;a href="https://voice-scribe.netlify.app" rel="noopener noreferrer"&gt;voice-scribe.netlify.app&lt;/a&gt;&lt;/strong&gt; in any browser, pick a language, and speak.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For information &amp;amp; questions contact me at:&lt;/strong&gt; &lt;strong&gt;&lt;a href="https://bix.pages.dev" rel="noopener noreferrer"&gt;bix.pages.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>deved</category>
      <category>learngoogleaistudio</category>
      <category>gemini</category>
      <category>googleaistudio</category>
    </item>
    <item>
      <title>AI helps me code faster, but not always understand better</title>
      <dc:creator>Bohdan Chuprynka</dc:creator>
      <pubDate>Sun, 05 Apr 2026 20:39:44 +0000</pubDate>
      <link>https://design.forem.com/__6146a1dd7/ai-helps-me-code-faster-but-not-always-understand-better-oik</link>
      <guid>https://design.forem.com/__6146a1dd7/ai-helps-me-code-faster-but-not-always-understand-better-oik</guid>
      <description>&lt;p&gt;I’ve been thinking about this a lot lately.&lt;/p&gt;

&lt;p&gt;AI coding tools are obviously useful. They help you move faster, get unstuck, write boilerplate, debug, and try things quickly.&lt;/p&gt;

&lt;p&gt;But I keep noticing the same tension:&lt;/p&gt;

&lt;p&gt;Sometimes AI helps you finish something without helping you really understand it.&lt;/p&gt;

&lt;p&gt;The code works.&lt;br&gt;
The task is done.&lt;br&gt;
But your confidence in the code is not always there.&lt;/p&gt;

&lt;p&gt;I think that matters a lot, especially for:&lt;br&gt;
 people learning to code&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;students&lt;/li&gt;
&lt;li&gt;developers trying to stay sharp&lt;/li&gt;
&lt;li&gt;anyone using AI but still wanting to actually understand what they’re building&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the reason I’ve been exploring an idea for a tool that feels more like a mentor inside the editor, not just something that generates code.&lt;/p&gt;

&lt;p&gt;Something that helps you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;understand what the code is doing&lt;/li&gt;
&lt;li&gt;catch weak spots in your thinking&lt;/li&gt;
&lt;li&gt;notice when you’re relying on AI too quickly&lt;/li&gt;
&lt;li&gt;learn while building, not only finish faster&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m trying to figure out whether this is a real problem other people feel too, or if it’s just something I’ve personally noticed.&lt;/p&gt;

&lt;p&gt;So I made a very short survey for anyone who has used AI for coding, debugging, or learning programming.&lt;/p&gt;

&lt;p&gt;It’s just 4 questions and takes about 1 minute:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://forms.gle/GynGGBWF46de9jz47" rel="noopener noreferrer"&gt;https://forms.gle/GynGGBWF46de9jz47&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you’ve used AI for coding even once, I’d really value your input.&lt;/p&gt;

&lt;p&gt;Also curious what people here think:&lt;/p&gt;

&lt;p&gt;Has AI made you better at coding, or mostly just faster?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>coding</category>
      <category>productivity</category>
      <category>vibecoding</category>
    </item>
    <item>
      <title>Chaining MCP Tools: Build AI Workflows That Search, Read, Analyze, and Write</title>
      <dc:creator>NeuroLink AI</dc:creator>
      <pubDate>Sun, 05 Apr 2026 20:27:53 +0000</pubDate>
      <link>https://design.forem.com/neurolink/chaining-mcp-tools-build-ai-workflows-that-search-read-analyze-and-write-e40</link>
      <guid>https://design.forem.com/neurolink/chaining-mcp-tools-build-ai-workflows-that-search-read-analyze-and-write-e40</guid>
      <description>&lt;h2&gt;
  
  
  What is MCP Tool Chaining?
&lt;/h2&gt;

&lt;p&gt;Imagine an AI that can not only understand a request like "Analyze our codebase for security vulnerabilities and report them," but also execute that request end-to-end. This requires more than just a single AI model. It needs an orchestration layer that allows the AI to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Search&lt;/strong&gt; external systems (e.g., GitHub, a file system).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Read&lt;/strong&gt; and comprehend various data formats (code, documents, database records).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Analyze&lt;/strong&gt; the information using its inherent intelligence.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Act&lt;/strong&gt; on its findings by writing code, creating issues, generating reports, or sending messages.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;MCP tool chaining is the mechanism that makes this possible. It's an architecture where AI models interact with a standardized set of tools (MCP servers) that expose real-world capabilities. When an AI needs to perform a task that requires external interaction, it invokes the appropriate tool, processes the output, and then uses another tool to continue the workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example Workflow 1: Code Analysis and Issue Creation
&lt;/h2&gt;

&lt;p&gt;Let's consider a practical example: an AI agent performing continuous code quality monitoring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Workflow:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Search GitHub&lt;/strong&gt;: The AI uses a GitHub MCP tool to search for new pull requests or recently committed code in a specific repository.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Read Code&lt;/strong&gt;: Once new code is identified, the AI uses the GitHub MCP tool to read the relevant files.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Analyze Patterns&lt;/strong&gt;: The AI then analyzes the code for potential bugs, security vulnerabilities, or deviations from coding standards.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Create Issue&lt;/strong&gt;: If issues are found, the AI uses the GitHub MCP tool to automatically create a new GitHub issue, detailing the problem, suggesting a fix, and assigning it to the relevant team.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This entire process, from detection to reporting, can be fully automated through MCP tool chaining.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example Workflow 2: Data-Driven Reporting
&lt;/h2&gt;

&lt;p&gt;Another powerful application lies in data analysis and reporting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Workflow:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Query Database&lt;/strong&gt;: An AI is tasked with generating a weekly sales report. It uses a PostgreSQL MCP tool to query the sales database, fetching relevant data like product sales, regional performance, and customer demographics.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Analyze Data&lt;/strong&gt;: The AI processes the raw data, identifying trends, anomalies, and key insights.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Generate Report&lt;/strong&gt;: Based on its analysis, the AI uses its generative capabilities to draft a comprehensive report, including summaries, visualizations (if integrated with a charting tool), and strategic recommendations.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Post to Slack&lt;/strong&gt;: Finally, the AI uses a Slack MCP tool to post the generated report (or a summary of it) to the relevant team's channel, ensuring stakeholders are promptly informed.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  NeuroLink: Orchestrating the AI Nervous System
&lt;/h2&gt;

&lt;p&gt;NeuroLink is designed to be the "pipe layer for the AI nervous system," specifically built to facilitate these complex, multi-tool AI workflows. It unifies over 13 major AI providers and provides a seamless way to connect and chain MCP tools.&lt;/p&gt;

&lt;p&gt;One of NeuroLink's key features is its &lt;code&gt;addExternalMCPServer&lt;/code&gt; method, which allows you to integrate any external tool or system as an MCP server. Once registered, NeuroLink enables the AI to automatically discover and chain these tools based on the task at hand.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Examples with NeuroLink API
&lt;/h3&gt;

&lt;p&gt;Here's how you might configure NeuroLink to connect to GitHub, a PostgreSQL database, and Slack, and then leverage them in an AI workflow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NeuroLink&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@juspay/neurolink&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NeuroLink&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Add GitHub as an MCP server&lt;/span&gt;
&lt;span class="c1"&gt;// This enables AI to interact with GitHub for searching, reading, and creating issues/PRs.&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addExternalMCPServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;github&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;npx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-y&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@modelcontextprotocol/server-github&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stdio&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Using stdio for local execution&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_TOKEN&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// Securely pass credentials&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Add PostgreSQL as an MCP server&lt;/span&gt;
&lt;span class="c1"&gt;// This allows AI to query and manipulate database records.&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addExternalMCPServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;postgres&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;npx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-y&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@modelcontextprotocol/server-postgres&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stdio&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Add Slack as an MCP server&lt;/span&gt;
&lt;span class="c1"&gt;// This empowers AI to post messages, summaries, or reports to Slack channels.&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addExternalMCPServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;slack&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Assuming a remote Slack MCP server&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://your-mcp-slack-server.com/api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SLACK_BOT_TOKEN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Now, the AI can chain these tools automatically based on the prompt:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
      1. Find all recently closed GitHub issues in the 'neurolink' repository related to 'performance'.
      2. Analyze the resolution steps and any associated code changes.
      3. Query the 'production_metrics' PostgreSQL database for performance data during the resolution period.
      4. Based on the analysis, draft a summary report on the effectiveness of the fixes and post it to the #engineering-updates Slack channel.
    `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// The AI's generated content, after tool execution&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, NeuroLink acts as the central orchestrator, receiving the high-level request, breaking it down into sub-tasks, identifying the appropriate MCP tools (GitHub, Postgres, Slack), executing them in sequence, and synthesizing the results. The AI agent, powered by NeuroLink, intelligently decides which tool to use at each step, forming a dynamic "tool chain."&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging Tool Chains
&lt;/h2&gt;

&lt;p&gt;Building complex AI workflows inevitably involves debugging. Here are some tips when working with NeuroLink and MCP tool chains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Verbose Logging&lt;/strong&gt;: Enable verbose logging in NeuroLink to see the exact tool calls the AI makes, their inputs, and their outputs. This is crucial for understanding the AI's decision-making process.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Isolate Tools&lt;/strong&gt;: Test each MCP server independently to ensure it functions correctly before integrating it into a complex chain.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Step-by-Step Execution&lt;/strong&gt;: For difficult cases, use NeuroLink's interactive CLI (&lt;code&gt;neurolink loop&lt;/code&gt;) or &lt;code&gt;prepareStep&lt;/code&gt; feature to step through the AI's thought process and tool invocations.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Validate Inputs/Outputs&lt;/strong&gt;: Ensure that the output of one tool call correctly matches the expected input format for the next tool in the chain. Discrepancies here are a common source of errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Performance: ToolCache and RequestBatcher for Optimization
&lt;/h2&gt;

&lt;p&gt;As AI agents perform more complex tasks, performance becomes critical. NeuroLink offers built-in mechanisms to optimize tool chain execution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;ToolCache&lt;/code&gt;&lt;/strong&gt;: This module allows NeuroLink to cache the results of frequently called, idempotent tools. If an AI requests the same data (e.g., a file content from GitHub) multiple times within a short period, &lt;code&gt;ToolCache&lt;/code&gt; can serve the result from memory instead of re-executing the tool, significantly reducing latency and API costs. You can configure various caching strategies like LRU, FIFO, or LFU.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ToolCache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NeuroLink&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@juspay/neurolink&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ToolCache&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lru&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// Cache for 5 minutes&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NeuroLink&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// ... other config&lt;/span&gt;
  &lt;span class="na"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;toolCache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;RequestBatcher&lt;/code&gt;&lt;/strong&gt;: For tools that can process multiple requests efficiently in a single call (e.g., querying a database for several items), &lt;code&gt;RequestBatcher&lt;/code&gt; automatically groups concurrent tool calls into a single batch request. This reduces the overhead of individual API calls, improving throughput.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;RequestBatcher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NeuroLink&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@juspay/neurolink&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;batcher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RequestBatcher&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;maxBatchSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxWaitMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// Batch up to 10 requests or wait 50ms&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NeuroLink&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// ... other config&lt;/span&gt;
  &lt;span class="na"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;requestBatcher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;batcher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By intelligently applying caching and batching, NeuroLink ensures that your AI workflows remain performant and cost-effective, even when interacting with numerous external systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;MCP tool chaining with NeuroLink unlocks a new frontier in AI capabilities. By providing a robust framework for connecting and orchestrating diverse tools, NeuroLink empowers developers to build sophisticated AI agents that can search, read, analyze, and write across virtually any digital system. This ability to chain operations fundamentally transforms how AI can be integrated into real-world applications, paving the way for truly autonomous and intelligent workflows.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;NeuroLink — The Universal AI SDK for TypeScript&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/juspay/neurolink" rel="noopener noreferrer"&gt;github.com/juspay/neurolink&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Install: &lt;code&gt;npm install @juspay/neurolink&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://docs.neurolink.ink" rel="noopener noreferrer"&gt;docs.neurolink.ink&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Blog: &lt;a href="https://blog.neurolink.ink" rel="noopener noreferrer"&gt;blog.neurolink.ink&lt;/a&gt; — 150+ technical articles&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>typescript</category>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>I built a faster alternative to cp and rsync — here's how it works</title>
      <dc:creator>krit.k83 (ΚρητικόςIGB)</dc:creator>
      <pubDate>Sun, 05 Apr 2026 20:27:08 +0000</pubDate>
      <link>https://design.forem.com/krit83/i-built-a-faster-alternative-to-cp-and-rsync-heres-how-it-works-39fa</link>
      <guid>https://design.forem.com/krit83/i-built-a-faster-alternative-to-cp-and-rsync-heres-how-it-works-39fa</guid>
      <description>&lt;p&gt;I'm a systems engineer. I spend a lot of time copying files — backups to USB drives, transfers to NAS boxes, moving data between servers over SSH. And I kept running into the same frustrations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cp -r&lt;/code&gt; is painfully slow on HDDs when you have tens of thousands of small files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rsync&lt;/code&gt; is powerful but complex, and still slow for bulk copies&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scp&lt;/code&gt; and SFTP top out at 1-2 MB/s on transfers that should be much faster&lt;/li&gt;
&lt;li&gt;No tool tells you upfront if the destination even has enough space&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built &lt;strong&gt;fast-copy&lt;/strong&gt; — a Python CLI that copies files at maximum sequential disk speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea
&lt;/h2&gt;

&lt;p&gt;When you run &lt;code&gt;cp -r&lt;/code&gt;, files are read in directory order — which is essentially random on disk. Every file seek on an HDD costs 5-10ms. Multiply that by 60,000 files and you're spending minutes just on head movement.&lt;/p&gt;

&lt;p&gt;fast-copy does something different: it resolves the physical disk offset of every file before copying. On Linux it uses &lt;code&gt;FIEMAP&lt;/code&gt;, on macOS &lt;code&gt;fcntl&lt;/code&gt;, on Windows &lt;code&gt;FSCTL&lt;/code&gt;. Then it sorts files by block position and reads them sequentially.&lt;/p&gt;

&lt;p&gt;That alone makes a big difference. But there's more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deduplication
&lt;/h2&gt;

&lt;p&gt;Many directories have duplicate files — node_modules across projects, cached downloads, backup copies. fast-copy hashes every file with xxHash-128 (or SHA-256 as fallback), copies each unique file once, and creates hard links for duplicates.&lt;/p&gt;

&lt;p&gt;In my test with 92K files, over half were duplicates — saving 379 MB and a lot of I/O time.&lt;/p&gt;

&lt;p&gt;It also keeps a SQLite database of hashes, so repeated copies to the same destination skip files that were already copied in previous runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  SSH tar streaming
&lt;/h2&gt;

&lt;p&gt;This is the part I'm most proud of. Instead of using SFTP (which has significant protocol overhead), fast-copy streams files as chunked ~100 MB tar batches over raw SSH channels.&lt;/p&gt;

&lt;p&gt;The remote side runs &lt;code&gt;tar xf -&lt;/code&gt; and files land directly on disk — no temp files, no SFTP overhead. This even works on servers that have SFTP disabled, like some Synology NAS configurations.&lt;/p&gt;

&lt;p&gt;Three modes are supported:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local → Remote&lt;/li&gt;
&lt;li&gt;Remote → Local&lt;/li&gt;
&lt;li&gt;Remote → Remote (relay through your machine)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real benchmarks
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Local copy — 92K files to USB:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;44,718 unique files copied + 47,146 hard-linked&lt;/li&gt;
&lt;li&gt;509.8 MB written, 378.9 MB saved by dedup&lt;/li&gt;
&lt;li&gt;17.9 seconds, 28.5 MB/s&lt;/li&gt;
&lt;li&gt;All files verified after copy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Remote to local — 92K files over LAN:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;509.8 MB downloaded in 14 minutes&lt;/li&gt;
&lt;li&gt;46,951 duplicates detected, saving 378.5 MB of transfer&lt;/li&gt;
&lt;li&gt;3x faster than SFTP&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;The simplest way — just run the Python script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python fast_copy.py /source /destination
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or download a standalone binary (no Python needed) from the Releases page — available for Linux, macOS, and Windows.&lt;/p&gt;

&lt;p&gt;For SSH transfers, install paramiko:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;paramiko
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For faster hashing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;xxhash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/gekap/fast-copy" rel="noopener noreferrer"&gt;https://github.com/gekap/fast-copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;License: Apache 2.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd love to hear feedback — especially from anyone dealing with large file transfers or backup workflows. What tools are you currently using? What's missing from them?&lt;/p&gt;




</description>
      <category>python</category>
      <category>linux</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Chaining MCP Tools: Search Read Analyze Write in TypeScript</title>
      <dc:creator>NeuroLink AI</dc:creator>
      <pubDate>Sun, 05 Apr 2026 20:26:55 +0000</pubDate>
      <link>https://design.forem.com/neurolink/chaining-mcp-tools-search-read-analyze-write-in-typescript-2gmb</link>
      <guid>https://design.forem.com/neurolink/chaining-mcp-tools-search-read-analyze-write-in-typescript-2gmb</guid>
      <description>&lt;h1&gt;
  
  
  Chaining MCP Tools: Search → Read → Analyze → Write in TypeScript
&lt;/h1&gt;

&lt;p&gt;Building sophisticated AI agents requires more than simple prompt-response interactions. Real-world automation demands multi-step workflows where AI performs sequential operations—gathering information, processing it, and taking action. This is where NeuroLink's Model Context Protocol (MCP) tool chaining capabilities transform what's possible with TypeScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Anatomy of a Tool Chain
&lt;/h2&gt;

&lt;p&gt;Consider a typical developer workflow: searching GitHub for relevant code, reading files, analyzing patterns, and creating issues. Previously, this required orchestrating multiple API calls, managing state, and handling errors across different services. NeuroLink unifies these operations into a single, coherent AI workflow.&lt;/p&gt;

&lt;p&gt;Let's build a code review agent that chains MCP tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NeuroLink&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@juspay/neurolink&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NeuroLink&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Connect GitHub MCP server&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addExternalMCPServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;github&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;npx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-y&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@modelcontextprotocol/server-github&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stdio&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_TOKEN&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Connect code analysis MCP server&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addExternalMCPServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;code-analyzer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.codeanalysis.tools/mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bearer YOUR_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Execute a multi-step workflow&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Search the "acme-corp/payments" repo for all files using "processPayment()"
           function, analyze the code for security vulnerabilities, and create a GitHub
           issue titled "Security Review: processPayment() usage" with findings.`&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-4-sonnet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anthropic&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// NeuroLink orchestrates the chain:&lt;/span&gt;
&lt;span class="c1"&gt;// 1. Calls github.search_code()&lt;/span&gt;
&lt;span class="c1"&gt;// 2. Calls github.read_file() for each result&lt;/span&gt;
&lt;span class="c1"&gt;// 3. Calls code-analyzer.analyze() on content&lt;/span&gt;
&lt;span class="c1"&gt;// 4. Calls github.create_issue() with findings&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Understanding the Chain
&lt;/h2&gt;

&lt;p&gt;The beauty of NeuroLink's approach is that the LLM decides the optimal sequence of tool calls. When given a complex task, the AI breaks it down into discrete steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Step 1: Search
  └─ MCP Tool: github.search_code
  └─ Query: "processPayment repo:acme-corp/payments"
  └─ Result: 5 files found

Step 2: Read
  ├─ MCP Tool: github.read_file (file 1)
  ├─ MCP Tool: github.read_file (file 2)
  └─ ... (parallel execution)

Step 3: Analyze
  └─ MCP Tool: code-analyzer.analyze
  └─ Input: Combined file contents
  └─ Result: 3 security findings

Step 4: Write
  └─ MCP Tool: github.create_issue
  └─ Title: "Security Review: processPayment() usage"
  └─ Body: Formatted analysis with code snippets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Building Intelligent Routers
&lt;/h2&gt;

&lt;p&gt;For production applications, you need more than basic tool execution. NeuroLink provides a &lt;code&gt;ToolRouter&lt;/code&gt; for intelligent routing across multiple MCP servers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ToolRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ToolCache&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@juspay/neurolink&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Route calls based on capability matching&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ToolRouter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;capability-based&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;servers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;primary-db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://mcp-primary.db.internal/mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;query&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transaction&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;analytics-db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://mcp-analytics.db.internal/mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aggregate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;report&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;github&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://mcp-github.tools.internal/mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;repo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;issue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Cache expensive operations&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ToolCache&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lru&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Use in your NeuroLink instance&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configureMCP&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;batcher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;maxBatchSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxWaitMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Human-in-the-Loop for Critical Actions
&lt;/h2&gt;

&lt;p&gt;Not all tool chains should execute autonomously. NeuroLink's HITL (Human-in-the-Loop) system ensures human approval for sensitive operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NeuroLink&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;hitl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;requireApproval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;github.create_issue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;github.merge_pr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;db.execute&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;confidenceThreshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reviewCallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Send to Slack for approval&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;approval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;slack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendApprovalRequest&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;conversationSummary&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;approval&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;approved&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// The AI will pause before creating issues&lt;/span&gt;
&lt;span class="c1"&gt;// and wait for human approval&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Create a critical bug report for the auth module&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Real-World Example: Documentation Sync Agent
&lt;/h2&gt;

&lt;p&gt;Here's a complete implementation that keeps documentation synchronized with code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NeuroLink&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@juspay/neurolink&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createDocSyncAgent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NeuroLink&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Add required MCP servers&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addExternalMCPServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;github&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;npx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-y&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@modelcontextprotocol/server-github&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stdio&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_TOKEN&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addExternalMCPServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;notion&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.notion.com/mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NOTION_TOKEN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// The agent workflow&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;syncDocumentation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;notionPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Sync API documentation between GitHub repo "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" and Notion page "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;notionPage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;".

               Steps:
               1. Search &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; for all files ending in "API.ts" or containing "@api" JSDoc
               2. Read each API file and extract endpoint definitions
               3. Compare with existing content in Notion page &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;notionPage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
               4. Update Notion with any new endpoints or changes
               5. Create a summary of updates made`&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-4-sonnet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anthropic&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;hitl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;requireApproval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;notion.update_page&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;syncDocumentation&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createDocSyncAgent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;syncDocumentation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;acme-corp/payments-api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;api-documentation-2024&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Best Practices for Tool Chaining
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Design for composability&lt;/strong&gt;: Build small, focused MCP servers that do one thing well&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use result caching&lt;/strong&gt;: Expensive operations like code analysis should be cached&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement HITL for mutations&lt;/strong&gt;: Always require approval for state-changing operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle failures gracefully&lt;/strong&gt;: Design chains that can retry or skip failed steps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor execution&lt;/strong&gt;: Use NeuroLink's telemetry to track tool usage and latency&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;MCP tool chaining with NeuroLink transforms AI agents from chatbots into autonomous workers capable of complex, multi-step workflows. By combining search, read, analyze, and write operations through a unified interface, you can automate sophisticated developer workflows while maintaining control and visibility.&lt;/p&gt;

&lt;p&gt;The future of AI isn't just smarter models—it's smarter orchestration of tools that work together seamlessly.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;NeuroLink — The Universal AI SDK for TypeScript&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/juspay/neurolink" rel="noopener noreferrer"&gt;github.com/juspay/neurolink&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Install: &lt;code&gt;npm install @juspay/neurolink&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://docs.neurolink.ink" rel="noopener noreferrer"&gt;docs.neurolink.ink&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Blog: &lt;a href="https://blog.neurolink.ink" rel="noopener noreferrer"&gt;blog.neurolink.ink&lt;/a&gt; — 150+ technical articles&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>typescript</category>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>The Service Layer: Where Separate Components Become a System</title>
      <dc:creator>Ozioma Ochin</dc:creator>
      <pubDate>Sun, 05 Apr 2026 20:24:51 +0000</pubDate>
      <link>https://design.forem.com/oozioma/the-service-layer-where-separate-components-become-a-system-4oeh</link>
      <guid>https://design.forem.com/oozioma/the-service-layer-where-separate-components-become-a-system-4oeh</guid>
      <description>&lt;p&gt;This is Part 4 of a series building a production-ready semantic search API with Java, Spring Boot, and pgvector.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/oozioma/building-a-semantic-search-api-with-spring-boot-and-pgvector-part-1-architecture-58b9"&gt;Part 1&lt;/a&gt; covered the architecture. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/oozioma/building-a-semantic-search-api-with-spring-boot-and-pgvector-part-2-designing-the-postgresql-2jlb"&gt;Part 2&lt;/a&gt; defined the schema. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/oozioma/building-a-semantic-search-api-with-spring-boot-and-pgvector-part-3-the-embedding-layer-1pj0"&gt;Part 3&lt;/a&gt; handled the embeddings — how text becomes vectors.&lt;/p&gt;

&lt;p&gt;Each piece worked in isolation. &lt;/p&gt;

&lt;p&gt;But systems don't fail in isolation — they fail at the boundaries.&lt;/p&gt;

&lt;p&gt;If you've ever built a feature that worked perfectly on its own but broke the moment you connected it to everything else — this article is about preventing that.&lt;/p&gt;

&lt;p&gt;At this point, we have a schema that can store documents and an embedding layer that can generate vectors. &lt;/p&gt;

&lt;p&gt;But nothing connects them. A document has nowhere to go. A query has no pipeline.&lt;/p&gt;

&lt;p&gt;This is where the service layer comes in.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;This is a production-style implementation — not a demo. The full project structure, tests, and configuration are available on &lt;a href="https://github.com/buenas/-semantic-search-service" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/strong&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What Does the Service Layer Actually Do?
&lt;/h3&gt;

&lt;p&gt;The database stores state, but it doesn't understand it. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;PENDING&lt;/code&gt;, &lt;code&gt;READY&lt;/code&gt;, and &lt;code&gt;FAILED&lt;/code&gt; only become meaningful once the service layer defines when those transitions happen and what triggers them.&lt;/p&gt;

&lt;p&gt;When a document arrives, the service decides the order of operations — save first, embed second, update on success, record failure explicitly if something goes wrong.&lt;/p&gt;

&lt;p&gt;Search follows the same pattern. A query doesn't go straight to the database. It's first converted into an embedding, then passed through a query that applies lifecycle constraints, metadata filters, and scoring thresholds. &lt;/p&gt;

&lt;p&gt;The service layer controls that entire pipeline.&lt;/p&gt;

&lt;p&gt;The service layer owns one thing: &lt;em&gt;&lt;strong&gt;the rules that make the system predictable.&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Without it, the system is just a collection of correct but disconnected components.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP Request
     │
     ▼
Controller Layer       ← validates input, delegates to service
     │
     ▼
Service Layer          ← all decisions happen here
     │                    │
     ▼                    ▼
Repository Layer      Embedding Layer
(JPA + JdbcTemplate)  (EmbeddingClient interface)
     │                    │
     ▼                    ▼
PostgreSQL + pgvector  OpenAI API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Interface That Keeps Everything Clean
&lt;/h3&gt;

&lt;p&gt;The service layer exposes one interface to the rest of the application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;DocumentService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;CreateDocumentResponse&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CreateDocumentRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="nc"&gt;DocumentResponse&lt;/span&gt; &lt;span class="nf"&gt;getById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="nc"&gt;SearchResponse&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SearchRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Controllers depend on the interface, not the implementation. &lt;/p&gt;

&lt;p&gt;Defining the contract as an interface and hiding the implementation behind it is what makes the system testable and changeable without cascading updates across the codebase.&lt;/p&gt;

&lt;p&gt;The more important detail is what does not cross this boundary.&lt;/p&gt;

&lt;p&gt;The Document entity never crosses this boundary — by design. Controllers receive &lt;code&gt;DTOs&lt;/code&gt;, not persistence objects. &lt;/p&gt;

&lt;p&gt;That separation means the database schema and the API contract can evolve independently. The schema can change without breaking clients. The API can change without rewriting persistence logic.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Why this matters to you:&lt;/strong&gt; If you've ever had a database change break your API — or an API change force a database rewrite — this boundary is what prevents that. Define it early and hold it firmly.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What Happens When Embedding Fails?
&lt;/h3&gt;

&lt;p&gt;From the outside, creating a document looks simple. Send a document, get an ID back.&lt;/p&gt;

&lt;p&gt;Inside the service, everything is built around one assumption: the second step might fail.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Override&lt;/span&gt;
&lt;span class="nd"&gt;@Transactional&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;CreateDocumentResponse&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CreateDocumentRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nc"&gt;Document&lt;/span&gt; &lt;span class="n"&gt;saved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;saveAsPending&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;embedAndPersist&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTitle&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreateDocumentResponse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="nc"&gt;DocumentStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;READY&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lines, two distinct operations.&lt;/p&gt;

&lt;p&gt;The first saves the document immediately with a status of &lt;code&gt;PENDING&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;The document exists in the database before any embedding call is made. &lt;/p&gt;

&lt;p&gt;If the application crashes at this point, the document is already there with a recoverable state.&lt;/p&gt;

&lt;p&gt;The second calls the OpenAI API, generates the embedding, and updates the document to &lt;code&gt;READY&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;If this step fails, the document moves to &lt;code&gt;FAILED&lt;/code&gt; instead, and the error is stored directly in the database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /documents
      │
      ▼
saveAsPending()
status = PENDING ← document is safe in the database
      │
      ▼
embedAndPersist()
      │
   ┌──┴──────────────┐
   │                 │
   ▼                 ▼
status = READY   status = FAILED
searchable       error stored in DB
                 excluded from search
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's an alternative that looks simpler — embed first, then save. &lt;/p&gt;

&lt;p&gt;It removes a step but removes visibility. If embedding fails in that model, the document never exists. There's no record, no state, nothing to debug. &lt;/p&gt;

&lt;p&gt;By saving first, every attempt leaves a trace. &lt;/p&gt;

&lt;p&gt;Failures don't disappear. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They become data.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This pattern — save first, embed second — is the difference between a failure you can debug and one that just disappears.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's how the failure handling actually works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;embedAndPersist&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;documentId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embeddingClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;embed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"\n\n"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jdbcTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;SQL_UPDATE_EMBEDDING&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;toPgVectorLiteral&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;documentId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IllegalStateException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                    &lt;span class="s"&gt;"Unexpected row count updating embedding for document id="&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;documentId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;IllegalStateException&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;markFailed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;documentId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RuntimeException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Embedding failed for document id="&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;documentId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three decisions here worth understanding:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Title and content are concatenated for embedding. &lt;code&gt;title + "\n\n" + content&lt;/code&gt; gives the model full context. A document titled "Payment Failure Handling Policy" with content about retry logic produces a richer embedding than the content alone.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;IllegalStateException&lt;/code&gt; is re-thrown unchanged. If the update affects zero or more than one row, something is wrong with the database state — not the embedding call. That error should propagate as-is rather than being wrapped as an embedding failure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Everything else triggers &lt;code&gt;markFailed&lt;/code&gt;. Network timeouts, rate limits, malformed responses — any exception that isn't an &lt;code&gt;IllegalStateException&lt;/code&gt; records the failure and re-throws. The caller sees the failure. The database gets a record of what went wrong.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Most API integration failures are silent. This makes them loud.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Search — The Pipeline That Ties Everything Together
&lt;/h3&gt;

&lt;p&gt;Search is the most complex operation in the service. It touches the embedding layer, the repository, and the database — and it has to coordinate all three correctly.&lt;/p&gt;

&lt;p&gt;What makes it manageable is not reducing that complexity, but containing it deliberately.&lt;/p&gt;

&lt;p&gt;The orchestration method is deliberately small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Override&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;SearchResponse&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SearchRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;qVector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embedQuery&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getQuery&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SearchResultItem&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fetchResults&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;qVector&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;countResults&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;qVector&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getFilters&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMinScore&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SearchResponse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPage&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSize&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;items&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four lines. Each delegates to a private method with a clear name. &lt;/p&gt;

&lt;p&gt;The method reads like a description of the search process — embed the query, fetch the results, count the total, return the response. &lt;/p&gt;

&lt;p&gt;The how is pushed down into methods that can be reasoned about in isolation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;embedQuery&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; 
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;toPgVectorLiteral&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embeddingClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;embed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt; 
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query goes through the same embedding client used for documents. &lt;/p&gt;

&lt;p&gt;That symmetry matters — the query and the stored documents exist in the same vector space. Without it, similarity search would be meaningless.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;SQL&lt;/code&gt; is constructed in two layers: the inner query selects candidates and computes similarity, while the outer query applies score thresholds and pagination.&lt;/p&gt;

&lt;p&gt;The split isn't stylistic. PostgreSQL cannot reference a &lt;code&gt;SELECT&lt;/code&gt; alias in a &lt;code&gt;WHERE&lt;/code&gt; clause at the same query level — which is why &lt;code&gt;cosine_distance&lt;/code&gt; must be resolved in a subquery before the score threshold can filter on it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cosine_distance&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'READY'&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'category'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sub&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;cosine_distance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;cosine_distance&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;If you've ever wondered why your JPA queries feel limiting for complex use cases — this is where you cross that line deliberately.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Why JPA Isn’t Enough for Vector Search
&lt;/h3&gt;

&lt;p&gt;The search query isn't static. &lt;/p&gt;

&lt;p&gt;Metadata filters, score thresholds, and pagination all change the SQL at runtime. &lt;/p&gt;

&lt;p&gt;At that point the abstraction provided by JPA starts to break down — you're no longer mapping objects, you're constructing a query.&lt;/p&gt;

&lt;p&gt;That's where QueryBuilder comes in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;QueryBuilder&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

   &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;StringBuilder&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
   &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;

   &lt;span class="nc"&gt;QueryBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;baseSql&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;firstParam&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
       &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StringBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseSql&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
       &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;firstParam&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
   &lt;span class="o"&gt;}&lt;/span&gt;

   &lt;span class="nc"&gt;QueryBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;baseSql&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;QueryBuilder&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
       &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StringBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseSql&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
       &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
   &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two constructors mirror the structure of the query – inner and outer. &lt;/p&gt;

&lt;p&gt;The first builds the inner query. &lt;/p&gt;

&lt;p&gt;The second builds the outer query, inheriting parameters from the inner one without tracking them manually. &lt;/p&gt;

&lt;p&gt;Where injection risk actually lives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;applyFilters&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

   &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Entry&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;entrySet&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
       &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getKey&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

       &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"^[a-zA-Z0-9_-]{1,64}$"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
           &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IllegalArgumentException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Invalid metadata filter key: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
       &lt;span class="o"&gt;}&lt;/span&gt;

       &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  AND (metadata-&amp;gt;&amp;gt;'"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"') = ?\n"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
       &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getValue&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
   &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The filter key is appended directly into the &lt;code&gt;SQL&lt;/code&gt; string. &lt;code&gt;SQL&lt;/code&gt; doesn't allow placeholders for column names or &lt;code&gt;JSON&lt;/code&gt; path expressions — which means this is where injection risk enters the system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The regex is not a convenience. It is the only control point between user input and the database.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;^[a-zA-Z0-9_-]{1,64}$&lt;/code&gt; — only alphanumeric characters, underscores, and hyphens. &lt;/p&gt;

&lt;p&gt;Anything else is rejected before it reaches the database. Filter values, on the other hand, always go through JDBC parameters and are safe regardless of input. &lt;/p&gt;

&lt;p&gt;This split — validated keys, parameterised values — is what makes the query both flexible and secure.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This is one of those cases where the 'boring' regex is doing serious security work. Don't skip it.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Key validation handles injection risk. The other challenge in query construction is where to apply the score threshold.&lt;/p&gt;

&lt;p&gt;Score filtering is applied on the outer query — not the inner one. &lt;code&gt;cosine_distance&lt;/code&gt; is defined in the inner query's &lt;code&gt;SELECT&lt;/code&gt; clause. &lt;/p&gt;

&lt;p&gt;PostgreSQL cannot reference that alias in a &lt;code&gt;WHERE&lt;/code&gt; clause at the same level. Wrapping it as a subquery makes it a real column in the outer scope — which is what allows &lt;code&gt;minScore&lt;/code&gt; to work at all.&lt;/p&gt;

&lt;p&gt;This is the point where you stop “using an ORM” and start designing queries deliberately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Updating a Document means Updating Its Embedding Too
&lt;/h3&gt;

&lt;p&gt;Updating a document is not the same as updating a database row.&lt;/p&gt;

&lt;p&gt;When content changes, the stored embedding becomes stale. A document about "payment retry logic" gets updated to "refund processing." &lt;/p&gt;

&lt;p&gt;But the embedding still points toward payment retries. Searches for "refund policy" would miss it. Searches for "payment retries" would still find it — incorrectly.&lt;/p&gt;

&lt;p&gt;The update operation handles this explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;applyUpdates&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Document&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UpdateDocumentRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setTitle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTitle&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setContent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setMetadata&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMetadata&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DocumentStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;PENDING&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setEmbeddingError&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;documentRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The moment content changes, the embedding becomes invalid. &lt;/p&gt;

&lt;p&gt;The system makes that explicit by resetting the document to &lt;code&gt;PENDING&lt;/code&gt;, removing it from search until a new embedding is generated.&lt;/p&gt;

&lt;p&gt;This trades availability for correctness — a document disappearing briefly is preferable to returning incorrect results.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;findOrThrow&lt;/code&gt; is called again after &lt;code&gt;embedAndPersist&lt;/code&gt; so the response reflects the document's final state — including the updated status and &lt;code&gt;embeddingUpdatedAt&lt;/code&gt; timestamp — not the state before the embedding ran.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This is easy to miss when you first build it. If a document update doesn't trigger a re-embed, your search results will silently drift out of sync with your content.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  One Place for All Your Errors
&lt;/h3&gt;

&lt;p&gt;Errors in this system fall into two categories — errors the caller caused and errors the system encountered. &lt;/p&gt;

&lt;p&gt;Those two cases should not look the same.&lt;/p&gt;

&lt;p&gt;A missing document returns a &lt;code&gt;404&lt;/code&gt;. Invalid input returns a &lt;code&gt;400&lt;/code&gt;. An embedding failure returns a &lt;code&gt;500&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What matters more than the distinction is consistency — every error, regardless of where it originates, returns the same shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NOT_FOUND"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Document not found: 42"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That consistency is enforced in one place — &lt;code&gt;GlobalExceptionHandler&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RestControllerAdvice&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GlobalExceptionHandler&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@ExceptionHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ResourceNotFoundException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ErrorResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleNotFound&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;ResourceNotFoundException&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ErrorResponse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                        &lt;span class="s"&gt;"NOT_FOUND"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@ExceptionHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MethodArgumentNotValidException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ErrorResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleValidation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;MethodArgumentNotValidException&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBindingResult&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getFieldErrors&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getField&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;": "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDefaultMessage&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;collect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Collectors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;joining&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;", "&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ErrorResponse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                        &lt;span class="s"&gt;"VALIDATION_ERROR"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;message&lt;/span&gt;
                &lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@ExceptionHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ErrorResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleGeneral&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ErrorResponse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                        &lt;span class="s"&gt;"INTERNAL_ERROR"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                        &lt;span class="s"&gt;"An unexpected error occurred"&lt;/span&gt;
                &lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;@RestControllerAdvice&lt;/code&gt; annotation makes it active across all controllers without being wired into any of them. &lt;/p&gt;

&lt;p&gt;The service layer throws exceptions. The handler translates them. The controllers never see error handling code.&lt;/p&gt;

&lt;p&gt;A client that always receives code and message can handle all errors with one piece of logic. &lt;/p&gt;

&lt;p&gt;A client that receives different shapes from different endpoints has to handle each one separately.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;One handler, consistent responses everywhere — your frontend team will thank you.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  How the LifecycleKeeps Bad Data Out of Search
&lt;/h3&gt;

&lt;p&gt;The document lifecycle isn't just about tracking failures. It's what keeps invalid data out of search results entirely.&lt;/p&gt;

&lt;p&gt;Every search query filters on two conditions before any similarity calculation runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'READY'&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;PENDING&lt;/code&gt; document is excluded. A &lt;code&gt;FAILED&lt;/code&gt; document is excluded.&lt;/p&gt;

&lt;p&gt;This is where the schema design from Part 2 pays off — the composite index on &lt;code&gt;(status, created_at DESC)&lt;/code&gt; exists specifically to support this filtering pattern. &lt;/p&gt;

&lt;p&gt;Without it, every search scans the full table and discards non-ready documents. With it, PostgreSQL jumps directly to the relevant subset.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PENDING ──────────────────────────────┐
   │                                  │
   ▼                                  │
embedAndPersist()                     │
   │                                  │
┌──┴──────────────┐                   │
│                 │                   │
▼                 ▼                   ▼
READY          FAILED            not searchable
searchable     error in DB
               not searchable

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

&lt;/div&gt;



&lt;p&gt;The lifecycle isn't just about correctness. It's a performance optimization.&lt;/p&gt;

&lt;p&gt;If you've ever had stale or incomplete data show up in search results with no explanation — a lifecycle model like this is what prevents it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The System Now Works
&lt;/h3&gt;

&lt;p&gt;With the service layer in place, the system finally behaves like a system.&lt;/p&gt;

&lt;p&gt;A document arrives at &lt;code&gt;POST /documents&lt;/code&gt;. The controller validates the request and delegates to the service. &lt;/p&gt;

&lt;p&gt;The service saves the document as &lt;code&gt;PENDING&lt;/code&gt;, calls the embedding client, and updates the status to &lt;code&gt;READY&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;The document is now stored with a valid embedding and visible to search.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmo1qdgrtffcgltmbhjky.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmo1qdgrtffcgltmbhjky.png" alt="Search and Post"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A search query arrives at &lt;code&gt;POST /search&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;The service embeds the query, builds the SQL dynamically through &lt;code&gt;QueryBuilder&lt;/code&gt;, applies filters and score thresholds, and returns ranked results with three score fields — &lt;code&gt;cosineDistance&lt;/code&gt;, &lt;code&gt;cosineSimilarity&lt;/code&gt;, and &lt;code&gt;score&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Every layer has exactly one job. Every failure is visible. Every response has a consistent shape.&lt;/p&gt;

&lt;p&gt;The system that started as a schema and an embedding client in &lt;a href="https://dev.to/oozioma/building-a-semantic-search-api-with-spring-boot-and-pgvector-part-1-architecture-58b9"&gt;Part 1&lt;/a&gt; is now a complete, working API.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's Next
&lt;/h3&gt;

&lt;p&gt;The service layer completes the system. Everything now works end to end.&lt;/p&gt;

&lt;p&gt;But working systems still have flaws.&lt;/p&gt;

&lt;p&gt;In the next article, I’ll step back from the implementation and break down what this system gets right, what it gets wrong, and what I would change if I were to build it again.&lt;/p&gt;

&lt;p&gt;See you there.&lt;/p&gt;

</description>
      <category>java</category>
      <category>vectordatabase</category>
      <category>springboot</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Understanding Data Modelling in Power BI: Joins, Relationships, and Schemas Explained</title>
      <dc:creator>Dalton Imbiru</dc:creator>
      <pubDate>Sun, 05 Apr 2026 20:22:55 +0000</pubDate>
      <link>https://design.forem.com/dalton_imbiru_82680ef8a50/understanding-data-modelling-in-power-bi-joins-relationships-and-schemas-explained-51gf</link>
      <guid>https://design.forem.com/dalton_imbiru_82680ef8a50/understanding-data-modelling-in-power-bi-joins-relationships-and-schemas-explained-51gf</guid>
      <description>&lt;p&gt;Data modelling is the foundation of effective data analysis in Power BI. A well-structured model ensures faster performance, accurate calculations, and easier report building. This article breaks down everything you need to know—from SQL joins and Power BI relationships to schemas and practical implementation steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Data Modelling?
&lt;/h2&gt;

&lt;p&gt;Data modelling is the process of organising data into tables and defining how those tables relate to each other so that analysis becomes meaningful and efficient.&lt;/p&gt;

&lt;p&gt;In Power BI, data modeling involves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; Structuring tables (Fact and Dimension)&lt;/li&gt;
&lt;li&gt; Defining relationships between tables&lt;/li&gt;
&lt;li&gt; Optimizing performance and usability&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  SQL Joins Explained (With Examples)
&lt;/h2&gt;

&lt;p&gt;Joins combine data from two or more tables based on a common column.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Customers&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CustomerID&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;John&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Mary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Alex&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Orders&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;OrderID&lt;/th&gt;
&lt;th&gt;CustomerID&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;103&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  INNER JOIN
&lt;/h2&gt;

&lt;p&gt;Returns only matching records from both tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CustomerID&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;OrderID&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;John&lt;/td&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Mary&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; When you only want valid matches (e.g., customers who made purchases).&lt;/p&gt;

&lt;h2&gt;
  
  
  LEFT JOIN
&lt;/h2&gt;

&lt;p&gt;Returns all records from the left table + matching from the right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CustomerID&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;OrderID&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;John&lt;/td&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Mary&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Alex&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; Show all customers, even those without orders.&lt;/p&gt;

&lt;h2&gt;
  
  
  RIGHT JOIN
&lt;/h2&gt;

&lt;p&gt;Returns all records from the right table + matching from the left.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CustomerID&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;OrderID&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;John&lt;/td&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Mary&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;103&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; Show all orders, even if customer info is missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  FULL OUTER JOIN
&lt;/h2&gt;

&lt;p&gt;Returns all records from both tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CustomerID&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;OrderID&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;John&lt;/td&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Mary&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Alex&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;103&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; Complete data reconciliation.&lt;/p&gt;

&lt;h2&gt;
  
  
  LEFT ANTI JOIN
&lt;/h2&gt;

&lt;p&gt;Returns rows from left table that have &lt;strong&gt;no match&lt;/strong&gt; in right table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CustomerID&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Alex&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; Identify customers who never purchased.&lt;/p&gt;

&lt;h2&gt;
  
  
  RIGHT ANTI JOIN
&lt;/h2&gt;

&lt;p&gt;Returns rows from right table with &lt;strong&gt;no match&lt;/strong&gt; in left table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;OrderID&lt;/th&gt;
&lt;th&gt;CustomerID&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;103&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; Identify orphan records (e.g., invalid orders).&lt;/p&gt;

&lt;h2&gt;
  
  
  Joins in Power BI (Power Query)
&lt;/h2&gt;

&lt;p&gt;Power BI implements joins in &lt;strong&gt;Power Query&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Steps:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Home → Transform Data&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select a table&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Merge Queries&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select second table&lt;/li&gt;
&lt;li&gt;Choose matching column(s)&lt;/li&gt;
&lt;li&gt;Select join type:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Inner&lt;/li&gt;
&lt;li&gt;Left Outer&lt;/li&gt;
&lt;li&gt;Right Outer&lt;/li&gt;
&lt;li&gt;Full Outer&lt;/li&gt;
&lt;li&gt;Left Anti&lt;/li&gt;
&lt;li&gt;Right Anti

&lt;ol&gt;
&lt;li&gt;Expand columns to finalize&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Relationships in Power BI
&lt;/h2&gt;

&lt;p&gt;Unlike SQL joins (which combine tables physically), Power BI relationships connect tables logically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Types of Relationships
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. One-to-Many (1:M)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;One record in Table A → Many in Table B&lt;/li&gt;
&lt;li&gt;Example: Customers → Orders&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most common relationship&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Many-to-Many (M:M)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Many records in both tables&lt;/li&gt;
&lt;li&gt;Example: Students ↔ Courses&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. One-to-One (1:1)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;One record matches exactly one record&lt;/li&gt;
&lt;li&gt;Example: Employee ↔ Employee Details&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cardinality
&lt;/h2&gt;

&lt;p&gt;Defines how tables relate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One-to-Many&lt;/li&gt;
&lt;li&gt;Many-to-One&lt;/li&gt;
&lt;li&gt;Many-to-Many&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cross-Filter Direction
&lt;/h2&gt;

&lt;p&gt;Controls how filters flow between tables.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single direction&lt;/strong&gt; → One way (recommended)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both directions&lt;/strong&gt; → Two-way filtering (use carefully)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Active vs Inactive Relationships
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Active relationship&lt;/strong&gt; → Default used in visuals&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inactive relationship&lt;/strong&gt; → Exists but is not used unless activated via DAX (&lt;code&gt;USERELATIONSHIP&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Order Date (Active)&lt;/li&gt;
&lt;li&gt;Ship Date (Inactive)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Creating Relationships in Power BI
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Method 1: Model View
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Model View&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Drag one column to another&lt;/li&gt;
&lt;li&gt;Relationship is created automatically&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Method 2: Manage Relationships
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Home → Manage Relationships&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;New&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Tables&lt;/li&gt;
&lt;li&gt;Columns&lt;/li&gt;
&lt;li&gt;Cardinality&lt;/li&gt;
&lt;li&gt;Cross-filter direction

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;OK&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Joins vs Relationships (Key Difference)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Joins&lt;/th&gt;
&lt;th&gt;Relationships&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where used&lt;/td&gt;
&lt;td&gt;Power Query&lt;/td&gt;
&lt;td&gt;Data Model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Result&lt;/td&gt;
&lt;td&gt;Combines tables&lt;/td&gt;
&lt;td&gt;Keeps tables separate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;Can increase size&lt;/td&gt;
&lt;td&gt;More efficient&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flexibility&lt;/td&gt;
&lt;td&gt;Static&lt;/td&gt;
&lt;td&gt;Dynamic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Fact vs Dimension Tables
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Fact Table
&lt;/h2&gt;

&lt;p&gt;Contains measurable data (numbers)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Examples:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sales&lt;/li&gt;
&lt;li&gt;Revenue&lt;/li&gt;
&lt;li&gt;Quantity&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Dimension Table
&lt;/h2&gt;

&lt;p&gt;Contains descriptive attributes&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Examples:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Customer Name&lt;/li&gt;
&lt;li&gt;Product Category&lt;/li&gt;
&lt;li&gt;Date&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Example Model
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;FactSales&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OrderID&lt;/li&gt;
&lt;li&gt;CustomerID&lt;/li&gt;
&lt;li&gt;ProductID&lt;/li&gt;
&lt;li&gt;SalesAmount&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;DimCustomer&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CustomerID&lt;/li&gt;
&lt;li&gt;Name&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;DimProduct&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ProductID&lt;/li&gt;
&lt;li&gt;Category&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Data Modeling Schemas
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Star Schema
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One central fact table&lt;/li&gt;
&lt;li&gt;Connected to multiple dimension tables&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Snowflake Schema
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Dimensions are normalized (split into multiple tables)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product → Category → Department&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Flat Table (Denormalized / DLAT)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;All data in one table&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Use Cases
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Schema&lt;/th&gt;
&lt;th&gt;When to Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Star&lt;/td&gt;
&lt;td&gt;Most Power BI reports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;td&gt;Complex hierarchical data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flat Table&lt;/td&gt;
&lt;td&gt;Small datasets or quick analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Role-Playing Dimensions
&lt;/h2&gt;

&lt;p&gt;A role-playing dimension is a table used multiple times for different purposes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example: Date Table&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Order Date&lt;/li&gt;
&lt;li&gt;Ship Date&lt;/li&gt;
&lt;li&gt;Delivery Date&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Power BI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Duplicate the Date table&lt;/li&gt;
&lt;li&gt;Create separate relationships&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Data Modeling Issues
&lt;/h2&gt;

&lt;p&gt;Ambiguous relationships&lt;br&gt;
Happens with multiple paths between tables&lt;/p&gt;

&lt;p&gt;Many-to-many confusion&lt;br&gt;
Can lead to incorrect aggregations&lt;/p&gt;

&lt;p&gt;Circular relationships&lt;br&gt;
Causes errors&lt;/p&gt;

&lt;p&gt;Poor performance&lt;br&gt;
Caused by flat tables or too many joins&lt;/p&gt;

&lt;p&gt;Solution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use star schema&lt;/li&gt;
&lt;li&gt;Avoid unnecessary bi-directional filters&lt;/li&gt;
&lt;li&gt;Keep relationships simple&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step-by-Step: Building a Model in Power BI
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Load Data
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Home → Get Data&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Clean Data (Power Query)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Remove duplicates&lt;/li&gt;
&lt;li&gt;Handle nulls&lt;/li&gt;
&lt;li&gt;Merge tables if needed&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: Create Relationships
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Go to Model View&lt;/li&gt;
&lt;li&gt;Drag and connect tables&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 4: Validate Model
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Check cardinality&lt;/li&gt;
&lt;li&gt;Ensure no ambiguous paths&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 5: Optimize
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Use star schema&lt;/li&gt;
&lt;li&gt;Reduce columns&lt;/li&gt;
&lt;li&gt;Avoid many-to-many unless necessary&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Data modeling in Power BI is not just technical, it’s strategic. Understanding joins helps you prepare data, while relationships allow you to analyze it efficiently. By structuring your data into fact and dimension tables and choosing the right schema (preferably star), you create a model that is both powerful and scalable.&lt;/p&gt;

&lt;p&gt;Mastering these concepts transforms Power BI from a simple visualization tool into a robust analytics engine and ultimately changes how you interpret and interact with data.&lt;/p&gt;

</description>
      <category>data</category>
      <category>microsoft</category>
      <category>sql</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I passed 13 AWS certifications. Here's what I actually use at work (and what collects dust).</title>
      <dc:creator>Vaibhav Kaushik</dc:creator>
      <pubDate>Sun, 05 Apr 2026 20:20:38 +0000</pubDate>
      <link>https://design.forem.com/some_tech_stuff_/i-passed-13-aws-certifications-heres-what-i-actually-use-at-work-and-what-collects-dust-148</link>
      <guid>https://design.forem.com/some_tech_stuff_/i-passed-13-aws-certifications-heres-what-i-actually-use-at-work-and-what-collects-dust-148</guid>
      <description>&lt;p&gt;It was a random Tuesday. No warning, no gradual degradation. RDS just... stopped accepting connections. ECS tasks were crashing in a cascade, CloudWatch alarms were firing, and my Slack was lighting up with "is prod down?" messages from three different people simultaneously.&lt;/p&gt;

&lt;p&gt;I didn't open a study guide. I opened CloudWatch Logs, pulled up the VPC flow logs, and started working backwards. Five years of late nights debugging security groups, IAM policies that were almost right, and routing tables that made sense on paper but not in practice that's what I was actually reaching for.&lt;/p&gt;

&lt;p&gt;That incident got me thinking: I've spent a significant chunk of my career chasing AWS certifications 13 of them, including Professional and Specialty levels. But which ones actually matter when the pressure is on?&lt;/p&gt;

&lt;p&gt;Here's my honest breakdown.&lt;/p&gt;

&lt;p&gt;I work as an AWS Solutions Architect at Wipro, designing multi-account architectures, building Terraform modules, and managing CI/CD pipelines for enterprise clients. My day-to-day stack is AWS, Terraform, Kubernetes, Docker, Python, and GitLab CI.&lt;/p&gt;

&lt;p&gt;The three that actually changed how I work&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Solutions Architect – Professional&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This one rewired how I think. Not because of the exam content, but because preparing for it forces you to reason about tradeoffs at scale cost vs performance vs reliability vs operational overhead. I now approach every architecture decision with that mental framework, even when no one asks me to.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;DevOps Engineer – Professional&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you work with CI/CD, IaC, or anything that touches deployment pipelines, this cert is the closest to real work. CodePipeline, CloudFormation, Systems Manager, Auto Scaling these come up constantly. Studying for this gave me a structured mental model for a domain I was already working in.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Security – Specialty&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Enterprise clients don't trust you until they trust your security posture. This cert gave me the vocabulary and depth to have those conversations confidently SCPs, permission boundaries, GuardDuty findings, encryption at rest vs in transit. In my experience, security knowledge is the #1 differentiator between a junior and senior cloud architect.&lt;/p&gt;

&lt;p&gt;The honest truth about certification chasing&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Certifications are a map. They are not the territory. The territory is production.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I've met engineers with 2 certs who could debug a broken VPC faster than people with 10. I've also met certified professionals who couldn't explain why their Terraform plan was destroying a resource it shouldn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Certs gave me:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A structured way to learn services I wouldn't have touched otherwise&lt;/li&gt;
&lt;li&gt;Credibility in client conversations and job applications&lt;/li&gt;
&lt;li&gt;A forcing function to fill gaps in my knowledge&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What certs didn't give me:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The ability to stay calm at 2am when prod is down&lt;/li&gt;
&lt;li&gt;Intuition for when &lt;em&gt;not&lt;/em&gt; to use a managed service&lt;/li&gt;
&lt;li&gt;Experience debugging a Kubernetes pod that won't start because of a misconfigured IAM role for service accounts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My recommendation if you're starting out&lt;/p&gt;

&lt;p&gt;Don't do 13. Do these four, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Practitioner&lt;/strong&gt; — only if you're new to cloud entirely&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solutions Architect – Associate&lt;/strong&gt; — your real starting point&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DevOps Engineer – Professional&lt;/strong&gt; — if you work in infra/platform/DevOps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security – Specialty&lt;/strong&gt; — regardless of your role, this pays off&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then go build something. Break it. Fix it. That's the cert that matters most.&lt;/p&gt;

&lt;p&gt;I'm a Solutions Architect at Wipro and AWS Community Builder based in London. I write about real-world AWS, Terraform, and cloud architecture. Follow along if that's useful to you.&lt;/p&gt;

&lt;p&gt;What cert has been most useful in your day-to-day work? Drop it in the comments.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>devops</category>
      <category>career</category>
    </item>
    <item>
      <title>RDLC Without BC: When the Feedback Loop Is Longer Than the Workday</title>
      <dc:creator>Stack Collider</dc:creator>
      <pubDate>Sun, 05 Apr 2026 20:18:29 +0000</pubDate>
      <link>https://design.forem.com/stackcollider/rdlc-without-bc-when-the-feedback-loop-is-longer-than-the-workday-19im</link>
      <guid>https://design.forem.com/stackcollider/rdlc-without-bc-when-the-feedback-loop-is-longer-than-the-workday-19im</guid>
      <description>&lt;h2&gt;
  
  
  RDLC Without BC: When the Feedback Loop Is Longer Than the Workday
&lt;/h2&gt;

&lt;h1&gt;
  
  
  csharp #dotnet #businesscentral #rdlc #wpf
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Nexus Claude: Every time a developer says "let me check how the report looks" — somewhere in an office, Business Central starts deploying.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;Developing RDLC reports for Business Central looks straightforward: edit the &lt;code&gt;.rdlc&lt;/code&gt; file in Visual Studio, deploy the extension to BC, run the report, see the result.&lt;/p&gt;

&lt;p&gt;The problem is in the word "deploy".&lt;/p&gt;

&lt;p&gt;Every change — even nudging a field by two pixels — requires a full cycle: build, deploy, launch BC, navigate to the report, enter parameters. Five minutes at best. Multiply by twenty iterations a day and the workday becomes waiting.&lt;/p&gt;

&lt;p&gt;The goal is simple:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Open RDLC and XML data → see the result. No BC. No deploy. Now.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Act One: Dead End in Visual Studio
&lt;/h2&gt;

&lt;p&gt;The first thought — Visual Studio Report Designer has a Preview mode. So you can connect XML as a DataSource and view the report right there.&lt;/p&gt;

&lt;p&gt;Create a Report Server Project. Open Designer. Try to add a DataSource — Designer won't allow it. Try connecting XML directly through Connection Properties — and get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The XmlDP query is invalid.
Unexpected end of file has occurred.
The following elements are not closed: Column, Columns, DataItem, DataItems, ReportDataSet.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the &lt;code&gt;.rdlc&lt;/code&gt; file manually. Inside — a hard lock:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;DataSources&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;DataSource&lt;/span&gt; &lt;span class="na"&gt;Name=&lt;/span&gt;&lt;span class="s"&gt;"DataSource"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ConnectionProperties&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;DataProvider&amp;gt;&lt;/span&gt;SQL&lt;span class="nt"&gt;&amp;lt;/DataProvider&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;ConnectString&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ConnectionProperties&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/DataSource&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/DataSources&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DataProvider=SQL&lt;/code&gt;. Can't change it through the UI. Designer won't allow it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nexus Claude: Microsoft isn't hiding the solution. Microsoft considers this scenario non-existent.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Official dead end. Moving on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Act Two: Build It Yourself
&lt;/h2&gt;

&lt;p&gt;If the tool doesn't exist — write it.&lt;/p&gt;

&lt;p&gt;The architecture is simple: a WPF application takes two files — &lt;code&gt;.rdlc&lt;/code&gt; and &lt;code&gt;.xml&lt;/code&gt; with BC data — renders a PDF via &lt;code&gt;ReportViewerCore.NETCore&lt;/code&gt; and displays the result through WebView2.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"ReportViewerCore.NETCore"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"*"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Web.WebView2"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"*"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rendering pipeline is transparent: XML is parsed into a &lt;code&gt;DataTable&lt;/code&gt;, &lt;code&gt;DataTable&lt;/code&gt; is passed to &lt;code&gt;LocalReport&lt;/code&gt;, &lt;code&gt;LocalReport&lt;/code&gt; renders PDF into a &lt;code&gt;MemoryStream&lt;/code&gt;, WebView2 displays the bytes.&lt;/p&gt;

&lt;p&gt;The only non-trivial part — column type inference. BC delivers everything as strings, but &lt;code&gt;LocalReport&lt;/code&gt; needs proper types — otherwise numeric fields don't calculate, dates don't sort.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;InferColumnType&lt;/code&gt; logic walks through all non-empty values in a column:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;int → decimal → DateTime → string&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Dates deserve special attention. BC uses the &lt;code&gt;dd/MM/yyyy&lt;/code&gt; format. Standard &lt;code&gt;DateTime.TryParse&lt;/code&gt; with &lt;code&gt;InvariantCulture&lt;/code&gt; won't recognize it. Explicit formats are required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;DateFormats&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"dd/MM/yyyy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"dd/MM/yyyy HH:mm:ss"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"yyyy-MM-dd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"yyyy-MM-ddTHH:mm:ss"&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryParseExact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateFormats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CultureInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvariantCulture&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DateTimeStyles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this, &lt;code&gt;19/02/2026&lt;/code&gt; silently falls through to &lt;code&gt;string&lt;/code&gt; — the report works, but date sorting breaks. Silently.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nexus Claude: A system that stays quiet when something goes wrong isn't more reliable than one that crashes. It's just harder to diagnose.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Act Three: The Installer Is Its Own Story
&lt;/h2&gt;

&lt;p&gt;A tool for yourself is one thing. A tool for the team requires an installer.&lt;/p&gt;

&lt;p&gt;The choice is obvious — Inno Setup. Free, proven, no dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem one: the preprocessor
&lt;/h3&gt;

&lt;p&gt;The first version of the script used &lt;code&gt;#define&lt;/code&gt; for variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight pascal"&gt;&lt;code&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="n"&gt;define&lt;/span&gt; &lt;span class="n"&gt;AppName&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;RDLC&lt;/span&gt; &lt;span class="n"&gt;Report&lt;/span&gt; &lt;span class="n"&gt;Tester&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="n"&gt;define&lt;/span&gt; &lt;span class="n"&gt;AppVersion&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="m"&gt;1.01&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inno Setup Compiler compiles without errors, but the &lt;code&gt;[Setup]&lt;/code&gt; section is empty — values weren't substituted. Cause: file encoding. &lt;code&gt;#define&lt;/code&gt; directives aren't read when saved as UTF-8 with BOM in certain configurations.&lt;/p&gt;

&lt;p&gt;Fix — remove the preprocessor entirely. Hardcode values directly.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Result:&lt;/em&gt; ❌ ~20 minutes&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem two: DownloadTemporaryFile
&lt;/h3&gt;

&lt;p&gt;The installer needs to check for WebView2 Runtime and download it if missing. First version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight pascal"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;DownloadTemporaryFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FileName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;DownloadProgress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
  &lt;span class="c1"&gt;// error
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compiler: type mismatch. &lt;code&gt;DownloadTemporaryFile&lt;/code&gt; returns &lt;code&gt;Int64&lt;/code&gt; (byte count), not &lt;code&gt;Boolean&lt;/code&gt;. On error — it throws an exception. Need &lt;code&gt;try/except&lt;/code&gt;, not a return value check.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Result:&lt;/em&gt; ❌ ~15 minutes&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem three: PublishSingleFile
&lt;/h3&gt;

&lt;p&gt;The app was built as &lt;code&gt;PublishSingleFile&lt;/code&gt; — one &lt;code&gt;.exe&lt;/code&gt;, no dependencies alongside. Logical for distribution.&lt;/p&gt;

&lt;p&gt;After installing via the installer, launch from &lt;code&gt;Program Files&lt;/code&gt; — silence. Event Viewer says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;System.DllNotFoundException: Dll was not found.
at MS.Internal.WindowsBase.NativeMethodsSetLastError.SetWindowLongPtrWndProc
at MS.Win32.HwndSubclass.SubclassWndProc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PublishSingleFile&lt;/code&gt; extracts native DLLs into a temp folder next to the exe at runtime. In &lt;code&gt;Program Files&lt;/code&gt; — no write permissions. WPF crashes before the window initializes. Silently.&lt;/p&gt;

&lt;p&gt;Fix — disable &lt;code&gt;PublishSingleFile&lt;/code&gt; in &lt;code&gt;.csproj&lt;/code&gt;. The installer packages the entire &lt;code&gt;publish\&lt;/code&gt; folder into one Setup.exe. The end result is the same — one file to download — but native DLLs sit alongside the exe where they belong.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Result:&lt;/em&gt; ❌ ~30 minutes&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem four: WebView2 and permissions
&lt;/h3&gt;

&lt;p&gt;After the DLL fix the app launches, but crashes again — now with a different error code. WebView2 creates a user data folder next to the exe during initialization. In &lt;code&gt;Program Files&lt;/code&gt; — no write permissions again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: WebView2 creates folder next to exe&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnsureCoreWebView2Async&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// After: explicit path in AppData&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;CoreWebView2Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Combine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFolderPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SpecialFolder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ApplicationData&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s"&gt;"RdlcReportTester"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"WebView2"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnsureCoreWebView2Async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Result:&lt;/em&gt; ✅&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nexus Claude: Two crashes in a row from the same root cause — no write permissions. Both silent. A good reminder: "installing in Program Files" isn't just a path — it's a contract with Windows about access rights.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Result
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BC XML DataSource → RdlcRendererWpf → PDF Preview
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the app, drag and drop &lt;code&gt;.rdlc&lt;/code&gt; and &lt;code&gt;.xml&lt;/code&gt;, see the report. No deployments, no BC sessions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv6p1dlw8wi90rk8bv9w1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv6p1dlw8wi90rk8bv9w1.png" alt="RDLC Report Tester v1.01 — PDF preview with test data"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A careful reader will spot &lt;code&gt;StackCollider Latvia SIA&lt;/code&gt; in the vendor list. An easter egg. Test data deserves character too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"ReportViewerCore.NETCore"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"*"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Web.WebView2"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"*"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full source → &lt;a href="https://github.com/stackcollider/rdlc-report-tester" rel="noopener noreferrer"&gt;github.com/stackcollider/rdlc-report-tester&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nexus Claude: A tool that speeds up development is code too. Sometimes it's more important to write it than the next report.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>csharp</category>
    </item>
  </channel>
</rss>
