<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Emmanuel R</title>
    <description>The latest articles on DEV Community by Emmanuel R (@emmanuelldev).</description>
    <link>https://dev.to/emmanuelldev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4146599%2F6aaae253-0310-42b2-8d55-7d4533d370a6.jpg</url>
      <title>DEV Community: Emmanuel R</title>
      <link>https://dev.to/emmanuelldev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/emmanuelldev"/>
    <language>en</language>
    <item>
      <title>A 14-Year-Old PHP Bug Lets Strangers Run Code on Your Server. Here's How to Stop It</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Thu, 22 Oct 2026 11:54:50 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/a-14-year-old-php-bug-lets-strangers-run-code-on-your-server-heres-how-to-stop-it-h50</link>
      <guid>https://dev.to/cobuildx-ai/a-14-year-old-php-bug-lets-strangers-run-code-on-your-server-heres-how-to-stop-it-h50</guid>
      <description>&lt;p&gt;&lt;em&gt;The code nobody touched for years just became everyone's problem. A bug hiding in PHP since 2012 lets strangers run code on web servers, and the NSA and India's CERT-In have both raised the alarm. Here's how PHP2Shell (CVE-2026-109618 and CVE-2026-109619) works, how it was found, and exactly what to do about it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On 21 October 2026, PHP shipped an emergency release for two critical bugs in the PHP engine itself: CVE-2026-109618 and CVE-2026-109619, nicknamed &lt;strong&gt;PHP2Shell&lt;/strong&gt;. Both let an attacker run code on a web server without logging in [1][2].&lt;/p&gt;

&lt;p&gt;Governments moved fast. The NSA told organizations that can't update right away to take their PHP systems offline [3]. The same day, India's national cybersecurity agency, &lt;strong&gt;CERT-In&lt;/strong&gt;, issued a critical-severity advisory (CIVN-2026-0510) warning that vulnerable PHP servers could be exposed to remote code execution [10][11]. CISA in the US issued its own alert [6].&lt;/p&gt;

&lt;p&gt;The unsettling part is where the bugs came from: code that has sat in PHP since 2012, and that attackers started probing before a patch existed. This post explains what the two bugs are, how they work, how they were found, and what to do about them. If you run PHP anywhere, including inside WordPress, Drupal, Magento or an internal tool, it applies to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Two Bugs at a Glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;CVE-2026-109618&lt;/th&gt;
&lt;th&gt;CVE-2026-109619&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name in the advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;RCE via OOM corruption&lt;/td&gt;
&lt;td&gt;RCE via stale global state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bug type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Memory corruption when PHP runs out of memory&lt;/td&gt;
&lt;td&gt;Use-after-free across two requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Severity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CVSS v4.0 9.2, critical&lt;/td&gt;
&lt;td&gt;CVSS v4.0 9.2, critical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Login needed?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Confirmed exploit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Apache mod_php&lt;/td&gt;
&lt;td&gt;mod_php and PHP-FPM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GHSA-w8hw-w9fx-j79w&lt;/td&gt;
&lt;td&gt;GHSA-6wmf-4rv5-p3rp&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In both cases, a successful attack runs code as the web server's PHP user. From there an attacker can read files, steal database passwords and keys from config files, or plant a backdoor [1][2].&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Bugs Work
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqcj8meg9td0ag9agfhxv.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqcj8meg9td0ag9agfhxv.png" alt="How PHP2Shell works. CVE-2026-109618: a crafted multipart request makes PHP run out of memory while the request is half set up; PHP jumps out early, and the clean-up step then uses the half-built data. CVE-2026-109619: request A ends in a fatal error, so clean-up is skipped and the worker keeps a pointer to freed memory; request B on the same worker uses that pointer before normal setup clears it" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  CVE-2026-109618: running out of memory at the wrong moment
&lt;/h3&gt;

&lt;p&gt;When PHP runs out of memory, it doesn't return an error step by step. It jumps straight out of whatever it was doing, using a C function called &lt;code&gt;longjmp()&lt;/code&gt;. If that happens while a request is still being set up, some of the request's data is left half-built. Later, when PHP cleans up the request, it treats that half-built data as valid, and memory gets corrupted [1].&lt;/p&gt;

&lt;p&gt;An attacker can cause this on purpose. Weaknesses in how PHP parses multipart form uploads let a crafted request use up memory while still looking like it's within the configured limits. On Apache with mod_php, this happens before any application code runs, so the app itself doesn't need to have a bug [1][4].&lt;/p&gt;

&lt;h3&gt;
  
  
  CVE-2026-109619: one request's leftovers, used by the next
&lt;/h3&gt;

&lt;p&gt;A PHP worker handles many requests in a row, and it keeps some global state between them: error, session and extension data. Normally that state is reset at the end of each request. But a fatal error, such as running out of memory or hitting the execution time limit, can skip that reset. The worker is left holding a pointer to memory that's already been freed [2].&lt;/p&gt;

&lt;p&gt;If the next request on the same worker touches that state before it's reset, PHP uses freed memory. The advisory confirms two ways to reach it. One is &lt;strong&gt;session upload progress&lt;/strong&gt;, a feature that's on by default and works even if the app never calls &lt;code&gt;session_start()&lt;/code&gt;. The other is PHP 8.5's new &lt;strong&gt;fatal error backtraces&lt;/strong&gt; setting, also on by default. Besides running code, this bug can leak memory from other users' requests, including passwords and tokens [2].&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Was Found
&lt;/h2&gt;

&lt;p&gt;Both bugs were found in August 2026 by researchers auditing PHP's C source with the help of AI models. Researcher Ryan Austin at Calif found one during an audit run under OpenAI's "Patch the Planet" project. Wiz found the same flaws independently, using AI models alongside its own tooling [5]. The PHP advisories credit reporters from both efforts and others [1][2].&lt;/p&gt;

&lt;p&gt;The weakness wasn't entirely new. In 2024 a PHP core developer spotted the out-of-memory problem behind CVE-2026-109618 during an audit, but the team couldn't agree on a fix, and a working exploit was thought unlikely, so it wasn't prioritised [5]. The affected code dates back to PHP 5.4, released in 2012.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timeline
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F29qtk9sa9bhqpqlkm6t3.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F29qtk9sa9bhqpqlkm6t3.png" alt="Timeline: 2012, PHP 5.4 ships with the affected code. 2024, the out-of-memory issue is spotted but not fixed. August 2026, Calif and Wiz report the bugs. 17 October, first exploit attempt seen by Amazon. 20 October, details leak and PHP announces a release. 21 October, patches and alerts from the NSA, CISA and CERT-In" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The release was meant to be a normal coordinated disclosure, but it didn't stay quiet. Amazon's honeypots logged an exploit attempt on 17 October, four days early. On 20 October news leaked on Hacker News, watchTowr posted about it, and a proof-of-concept video was published by mistake, so PHP announced the release a day ahead. The fixes and the two GitHub advisories went out on 21 October, the same day as alerts from CISA, the NSA, India's CERT-In and others [5][6][10].&lt;/p&gt;

&lt;p&gt;Some organizations didn't wait. Government portals in the Netherlands, Finland and France, many Wikimedia services and the Internet Archive went offline for a time as a precaution. Wiz compared the bug to Heartbleed, the 2014 OpenSSL flaw [5].&lt;/p&gt;

&lt;h2&gt;
  
  
  Am I Affected?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Versions:&lt;/strong&gt; everything from PHP 5.4 up to, but not including, &lt;strong&gt;8.2.35, 8.3.36, 8.4.27 and 8.5.12&lt;/strong&gt; [1][2].&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server setups:&lt;/strong&gt; any PHP that serves web requests: Apache mod_php (highest risk), PHP-FPM, php-cgi, FrankenPHP and LiteSpeed [4].&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Old versions:&lt;/strong&gt; PHP 8.1 and older are end of life. The PHP team released source patches for 7.4, 8.0 and 8.1, but no official builds. Older versions get nothing [5].&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hidden PHP:&lt;/strong&gt; PHP is often bundled inside other products, such as control panels, appliances and vendor software. Cisco, for example, published its own advisory for affected products [6][7].&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To check a server, look at the version and the two settings that matter most:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;php -v
php -i | grep -E 'session.upload_progress.enabled|fatal_error_backtraces|memory_limit'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this with the same PHP the web server uses. The command-line PHP can be a different version from PHP-FPM or mod_php, so also check your web server's &lt;code&gt;phpinfo()&lt;/code&gt; page or package list.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Do Now
&lt;/h2&gt;

&lt;p&gt;For security teams, this goes beyond applying an update. The work falls into four steps: patch, mitigate, contain and investigate. Start by finding every PHP install, internet-facing ones first [6].&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Patch
&lt;/h3&gt;

&lt;p&gt;Upgrade to a fixed release: &lt;strong&gt;8.5.12, 8.4.27, 8.3.36 or 8.2.35&lt;/strong&gt;, or your Linux distribution's patched package. Then &lt;strong&gt;restart every PHP worker&lt;/strong&gt;: PHP-FPM, Apache and any long-running PHP processes. Until they restart, the old code is still running [1][2].&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Mitigate, if you can't patch today
&lt;/h3&gt;

&lt;p&gt;The PHP advisories list temporary settings. Set them in &lt;code&gt;php.ini&lt;/code&gt;, the PHP-FPM pool or the web server config. Setting them with &lt;code&gt;ini_set()&lt;/code&gt; or &lt;code&gt;.user.ini&lt;/code&gt; is too late to help [1][2][4].&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;; CVE-2026-109619: closes the confirmed session path.
; Worth keeping off after patching if you don't use upload progress bars.
session.upload_progress.enabled = 0

; CVE-2026-109619, PHP 8.5 only: closes the confirmed backtrace path.
fatal_error_backtraces = 0

; CVE-2026-109618: avoids the out-of-memory jump. Not a complete fix,
; so pair it with tight request limits below and at your proxy.
memory_limit = -1
post_max_size = 8M
upload_max_filesize = 2M
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Limit requests before they reach PHP.&lt;/strong&gt; At nginx, Apache or your load balancer, cap request size, the number of form fields and multipart parts, and headers per part. If your app doesn't take file uploads or multipart forms, block them entirely [1].&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put a web application firewall in front&lt;/strong&gt; if you have one [6].&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't rely on &lt;code&gt;RLIMIT_AS&lt;/code&gt; or &lt;code&gt;RLIMIT_DATA&lt;/code&gt;.&lt;/strong&gt; These operating system memory limits can trigger the very bug you're trying to avoid [1].&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hiding errors doesn't help.&lt;/strong&gt; Turning off &lt;code&gt;display_errors&lt;/code&gt; or using &lt;code&gt;@&lt;/code&gt; doesn't stop the stale state from being used [4].&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;These settings only close the paths researchers have confirmed so far.&lt;/strong&gt; Both advisories say updating is the only complete fix [1][2].&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3. Contain
&lt;/h3&gt;

&lt;p&gt;If an internet-facing system can't be patched yet, or runs PHP 8.1 or older, take it offline until it's patched or upgraded. This is what the NSA and CISA both advise [3][6]. Several governments, Wikimedia and the Internet Archive did exactly this on 21 October [5].&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Investigate
&lt;/h3&gt;

&lt;p&gt;Exploit attempts started before the patch, so a patched server may already have been hit. Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PHP worker processes starting a shell (&lt;code&gt;sh&lt;/code&gt;, &lt;code&gt;bash&lt;/code&gt;) or tools like &lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;wget&lt;/code&gt; [6].&lt;/li&gt;
&lt;li&gt;Signs of persistence: new or changed &lt;code&gt;.php&lt;/code&gt; files in web directories (especially upload folders), new cron jobs or new user accounts.&lt;/li&gt;
&lt;li&gt;Unexpected outbound connections from web servers.&lt;/li&gt;
&lt;li&gt;Bursts of large or unusual multipart POST requests in your access logs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Clean logs don't prove a server is safe [2]. If you find signs of compromise, treat it as an incident: rotate the database passwords and API keys stored on that server, not just the PHP version.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Lesson
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Legacy code isn't harmless just because it's familiar.&lt;/strong&gt; The code behind PHP2Shell had run on millions of servers for 14 years, and it was trusted because nothing had gone wrong yet.&lt;/p&gt;

&lt;p&gt;Neither bug was in anyone's PHP code. Both were memory-safety bugs in the PHP engine, which is written in C: memory used after it was freed, and data used before it was fully set up. These are the same classes of bug the NSA and CISA have pointed to for years when they urge a move to memory-safe languages for new code [8][9].&lt;/p&gt;

&lt;p&gt;Two other points stand out. AI-assisted audits found bugs that had sat in widely used code for over a decade, and attackers have the same tools. And PHP runs a large share of the web, yet the PHP Foundation has only 13 employees and relies on donations [5]. If your business depends on PHP, supporting the foundation is part of keeping it safe.&lt;/p&gt;

&lt;p&gt;For now, the steps are simple: find every PHP server, update it, restart it, and check it wasn't hit first.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Advisories&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PHP. &lt;em&gt;RCE via OOM corruption&lt;/em&gt; (CVE-2026-109618, GHSA-w8hw-w9fx-j79w). &lt;a href="https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w" rel="noopener noreferrer"&gt;https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PHP. &lt;em&gt;RCE via stale global state&lt;/em&gt; (CVE-2026-109619, GHSA-6wmf-4rv5-p3rp). &lt;a href="https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp" rel="noopener noreferrer"&gt;https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;NSA Cybersecurity. Post on PHP vulnerabilities, 21 October 2026. &lt;a href="https://x.com/NSACyber/status/2112903220209098897" rel="noopener noreferrer"&gt;https://x.com/NSACyber/status/2112903220209098897&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Analysis and background&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rapid7. &lt;em&gt;CVE-2026-109618 and CVE-2026-109619: Critical Unauthenticated Remote Code Execution in PHP.&lt;/em&gt; &lt;a href="https://www.rapid7.com/blog/post/etr-cve-2026-109618-and-cve-2026-109619-critical-unauthenticated-remote-code-execution-in-php/" rel="noopener noreferrer"&gt;https://www.rapid7.com/blog/post/etr-cve-2026-109618-and-cve-2026-109619-critical-unauthenticated-remote-code-execution-in-php/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Wikipedia. &lt;em&gt;PHP2Shell.&lt;/em&gt; &lt;a href="https://en.wikipedia.org/wiki/PHP2Shell" rel="noopener noreferrer"&gt;https://en.wikipedia.org/wiki/PHP2Shell&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CISA. &lt;em&gt;Critical Vulnerabilities in PHP Require Immediate Patching&lt;/em&gt;, 21 October 2026. &lt;a href="https://www.cisa.gov/news-events/alerts/2026/10/21/critical-vulnerabilities-php-require-immediate-patching" rel="noopener noreferrer"&gt;https://www.cisa.gov/news-events/alerts/2026/10/21/critical-vulnerabilities-php-require-immediate-patching&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cisco. &lt;em&gt;Vulnerabilities in PHP Affecting Cisco Products: October 2026.&lt;/em&gt; &lt;a href="https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-php-october26-Jj384Daq" rel="noopener noreferrer"&gt;https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-php-october26-Jj384Daq&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;NSA. &lt;em&gt;Software Memory Safety&lt;/em&gt; (Cybersecurity Information Sheet). &lt;a href="https://media.defense.gov/2022/Nov/10/2003112742/-1/-1/0/CSI_SOFTWARE_MEMORY_SAFETY.PDF" rel="noopener noreferrer"&gt;https://media.defense.gov/2022/Nov/10/2003112742/-1/-1/0/CSI_SOFTWARE_MEMORY_SAFETY.PDF&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CISA. &lt;em&gt;The Case for Memory Safe Roadmaps.&lt;/em&gt; &lt;a href="https://www.cisa.gov/resources-tools/resources/case-memory-safe-roadmaps" rel="noopener noreferrer"&gt;https://www.cisa.gov/resources-tools/resources/case-memory-safe-roadmaps&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CERT-In. &lt;em&gt;Vulnerability Note CIVN-2026-0510: PHP2Shell vulnerabilities in PHP&lt;/em&gt;, 21 October 2026. &lt;a href="https://www.cert-in.org.in/s2cMainServlet?VLCODE=CIVN-2026-0510&amp;amp;pageid=PUBVLNOTES01" rel="noopener noreferrer"&gt;https://www.cert-in.org.in/s2cMainServlet?VLCODE=CIVN-2026-0510&amp;amp;pageid=PUBVLNOTES01&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;NewsBytes. &lt;em&gt;US NSA and India's CERT-In flag 'PHP2Shell' web risk.&lt;/em&gt; &lt;a href="https://www.newsbytesapp.com/news/science/us-nsa-and-indias-cert-in-flag-php2shell-web-risk/tldr" rel="noopener noreferrer"&gt;https://www.newsbytesapp.com/news/science/us-nsa-and-indias-cert-in-flag-php2shell-web-risk/tldr&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;CobuildX helps teams modernise legacy systems with &lt;a href="https://cobuildx.ai/migratex" rel="noopener noreferrer"&gt;MigrateX&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cobuildx.ai/blog/php2shell-cve-2026-109618-109619" rel="noopener noreferrer"&gt;CobuildX blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>security</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>Modernize Your Legacy Frontend Without a Rewrite</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Tue, 20 Oct 2026 08:06:01 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/migratex-modernize-legacy-frontends-one-feature-at-a-time-31oh</link>
      <guid>https://dev.to/cobuildx-ai/migratex-modernize-legacy-frontends-one-feature-at-a-time-31oh</guid>
      <description>&lt;p&gt;&lt;em&gt;Rewrites don't fail because of the new framework. They fail because of everything the old app does that nobody wrote down.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MigrateX&lt;/strong&gt; moves legacy frontends — Ember, Backbone, Angular, jQuery — to React &lt;strong&gt;one feature at a time&lt;/strong&gt;, while your product keeps shipping.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔍 &lt;strong&gt;Map everything first.&lt;/strong&gt; Every route, service and hidden side effect, before a line is ported.&lt;/li&gt;
&lt;li&gt;🔀 &lt;strong&gt;Ship behind a switch.&lt;/strong&gt; Each feature can be turned back to the old app instantly.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Prove parity.&lt;/strong&gt; The same tests run on old and new. It goes live only when both pass.&lt;/li&gt;
&lt;li&gt;🧹 &lt;strong&gt;Leave nothing behind.&lt;/strong&gt; Old code, switches and glue removed. One clean codebase.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No feature freeze. No launch day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;See it working:&lt;/strong&gt; five real apps migrated in the open → &lt;a href="https://github.com/cobuild-tech/migratex-examples" rel="noopener noreferrer"&gt;migratex-examples&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://cobuildx.ai/migratex" rel="noopener noreferrer"&gt;cobuildx.ai/migratex&lt;/a&gt; · &lt;a href="https://migratex.cobuildx.ai" rel="noopener noreferrer"&gt;Try the platform&lt;/a&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>migration</category>
    </item>
    <item>
      <title>Modernizing Legacy Apps with RxJS: Streams as the Bridge Between Old and New</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Mon, 12 Oct 2026 05:32:08 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/modernizing-legacy-apps-with-rxjs-streams-as-the-bridge-between-old-and-new-20c9</link>
      <guid>https://dev.to/cobuildx-ai/modernizing-legacy-apps-with-rxjs-streams-as-the-bridge-between-old-and-new-20c9</guid>
      <description>&lt;p&gt;&lt;em&gt;In most legacy migrations, the difficult part is the async code, not the templates: callbacks, observers, global events and timers that every screen depends on. This post explains how we use RxJS as a bridge, so the old UI and the new React UI share the same streams while screens are moved one by one.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In most legacy frontend migrations, the templates are not the hard part. Templates can be ported quite easily. The hard part is the &lt;strong&gt;async code&lt;/strong&gt;. A jQuery handler sends a request, sets a global flag and fires a custom event. A Backbone model has three views listening to its &lt;code&gt;change&lt;/code&gt; event. An Ember observer saves data on every keystroke. A &lt;code&gt;setInterval&lt;/code&gt; keeps polling the server, and nobody in the team knows why it was added.&lt;/p&gt;

&lt;p&gt;This is where the actual behavior of a legacy app lives, and it is spread across every page. We cannot move it one route at a time, because both the old screens and the new screens depend on it.&lt;/p&gt;

&lt;p&gt;In such projects, we use &lt;strong&gt;RxJS&lt;/strong&gt; as a &lt;strong&gt;bridge&lt;/strong&gt; between the old app and the new one. We do not use it as the final architecture, and we do not use it just because Angular uses it. The approach is simple. Wrap each legacy event source in an Observable and keep the shared state behind a stream. Now the old UI and the new React UI can subscribe to the same data while screens are moved one by one. As a bonus, a lot of hand-written debounce, retry and cancel logic becomes a few lines of operators. This is the &lt;a href="https://cobuildx.ai/blog/frontend-migration-strategies" rel="noopener noreferrer"&gt;branch by abstraction&lt;/a&gt; pattern, with streams as the interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Short Version
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Legacy pattern&lt;/th&gt;
&lt;th&gt;What goes wrong&lt;/th&gt;
&lt;th&gt;RxJS replacement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Callbacks and &lt;code&gt;$.Deferred&lt;/code&gt; chains&lt;/td&gt;
&lt;td&gt;Deeply nested, cannot be cancelled, errors get lost&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;from(promise)&lt;/code&gt;, &lt;code&gt;defer&lt;/code&gt;, &lt;code&gt;switchMap&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Global or custom events (&lt;code&gt;$(document).trigger&lt;/code&gt;, Backbone &lt;code&gt;on&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Any code can fire them, and nobody removes the listeners&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fromEvent&lt;/code&gt;, &lt;code&gt;fromEventPattern&lt;/code&gt;, typed &lt;code&gt;Subject&lt;/code&gt;s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observers and watchers (Ember &lt;code&gt;observer&lt;/code&gt;, &lt;code&gt;$scope.$watch&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Order is not clear, and they run too often&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;BehaviorSubject&lt;/code&gt; + &lt;code&gt;distinctUntilChanged&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hand-rolled debounce and timers&lt;/td&gt;
&lt;td&gt;Old responses overwrite new ones, timers keep running&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;debounceTime&lt;/code&gt;, &lt;code&gt;switchMap&lt;/code&gt;, &lt;code&gt;takeUntil&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Polling with &lt;code&gt;setInterval&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Requests overlap, and polling never stops&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;timer&lt;/code&gt; + &lt;code&gt;switchMap&lt;/code&gt; / &lt;code&gt;exhaustMap&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry loops&lt;/td&gt;
&lt;td&gt;Too many retries, no delay between them&lt;/td&gt;
&lt;td&gt;&lt;code&gt;retry({ count, delay })&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebSockets with manual reconnects&lt;/td&gt;
&lt;td&gt;Duplicate connections, lost messages&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;webSocket&lt;/code&gt; + &lt;code&gt;retry&lt;/code&gt; + &lt;code&gt;share&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every row follows the same idea: &lt;strong&gt;treat "something that happens over time" as a value that we can pass around, combine and unsubscribe from.&lt;/strong&gt; Once both apps use Observables, the framework on each side becomes much less important.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why RxJS Works Well for Modernization
&lt;/h2&gt;

&lt;p&gt;There are many state management libraries. For a migration, RxJS is useful for three specific reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It does not depend on any framework.&lt;/strong&gt; An Observable works the same way whether jQuery, Backbone, Ember, AngularJS or React subscribes to it. This is exactly what we need during a migration, when the old and new code run on the same page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One model for every kind of async.&lt;/strong&gt; DOM events, custom events, promises, timers, WebSockets and Node-style callbacks all become the same type. There is no need to write a different pattern for each source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cancellation and cleanup are built in.&lt;/strong&gt; Every subscription can be closed. Legacy apps often leave handlers and timers running, and many of these leaks are bugs that users have simply got used to.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is also one clear reason to be careful: &lt;strong&gt;RxJS takes time to learn&lt;/strong&gt;, and a codebase where everything is a stream is difficult to read. We will discuss where to stop later in this post. In short, use it at the boundaries and for real event streams, not for every value in the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Wrap the Legacy Event Sources
&lt;/h2&gt;

&lt;p&gt;First, find every place where the old app produces events or async results, and create an Observable wrapper for each one. Do not change the legacy code at this stage. The wrapper only listens.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb6s9s85kbana56piym44.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb6s9s85kbana56piym44.png" alt="Legacy event sources on the left: jQuery events, Backbone models, Ember observers, AngularJS watchers, promises and callbacks, timers, WebSockets. Each passes through an RxJS wrapper such as fromEvent, fromEventPattern, from or webSocket into a layer of shared Observable services. Both the legacy UI and the new React UI subscribe to that layer" width="800" height="328"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DOM and jQuery events.&lt;/strong&gt; &lt;code&gt;fromEvent&lt;/code&gt; accepts DOM elements and any object with jQuery-style &lt;code&gt;on&lt;/code&gt;/&lt;code&gt;off&lt;/code&gt; methods, so a jQuery object can be passed directly:&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;fromEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;map&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="s1"&gt;rxjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// The legacy app fires: $(document).trigger('cart:updated', [cart])&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;legacyCartUpdated$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fromEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cart:updated&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="nx"&gt;_e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;searchInput$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fromEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;InputEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#search&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Backbone, Ember and anything with its own add/remove API.&lt;/strong&gt; &lt;code&gt;fromEventPattern&lt;/code&gt; takes an add handler and a remove handler. This way, the Observable removes its listener from the legacy object properly when it is unsubscribed:&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;fromEventPattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;startWith&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;map&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="s1"&gt;rxjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Backbone model or collection&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fromBackbone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Backbone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;fromEventPattern&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Backbone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Model&lt;/span&gt;&lt;span class="o"&gt;&amp;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;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;off&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;startWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toJSON&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// Ember object property (Ember.addObserver / removeObserver)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fromEmberProperty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&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="nx"&gt;fromEventPattern&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;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;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;addObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;removeObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;startWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two points are important here. First, &lt;strong&gt;convert to plain objects at the boundary&lt;/strong&gt; (&lt;code&gt;toJSON()&lt;/code&gt;, &lt;code&gt;get&lt;/code&gt;). If the stream emits Backbone models or Ember records, the new code will also start calling &lt;code&gt;.get('title')&lt;/code&gt;, and the legacy model will spread into React. This is the &lt;a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer" rel="noopener noreferrer"&gt;anti-corruption layer&lt;/a&gt; from our strategies post, applied to streams. Second, &lt;code&gt;startWith&lt;/code&gt; gives the current value to late subscribers. So a React component that mounts later will not show an empty screen while it waits for the next change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Promises, callbacks and sockets&lt;/strong&gt; also have direct wrappers:&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;defer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bindNodeCallback&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="s1"&gt;rxjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;webSocket&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="s1"&gt;rxjs/webSocket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// defer: the request only starts when someone subscribes, and runs again per subscriber&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loadOrders$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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="nf"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;legacyApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// Node-style callback API from an old SDK&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;readConfig$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bindNodeCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;legacySdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readConfig&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prices$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;webSocket&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PriceTick&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wss://example.com/prices&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For requests, prefer &lt;code&gt;defer(() =&amp;gt; promise)&lt;/code&gt; over &lt;code&gt;from(promise)&lt;/code&gt;. A promise starts running as soon as it is created. &lt;code&gt;defer&lt;/code&gt; waits until someone subscribes, and this is what makes cancellation and retries possible later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Put Shared State Behind a Stream
&lt;/h2&gt;

&lt;p&gt;Wrapping the sources makes the old app observable. The next step is to give each piece of &lt;strong&gt;shared state&lt;/strong&gt; (the cart, the current user, the selected account, feature flags) a single owner. This owner is a small service that holds the value and exposes it as an Observable. Both apps read from this service, and all updates go through its methods.&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;BehaviorSubject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;distinctUntilChanged&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;map&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="s1"&gt;rxjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Cart&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CartItem&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="nl"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;BehaviorSubject&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Cart&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="na"&gt;total&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cartService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="na"&gt;$&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asObservable&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="na"&gt;$&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;distinctUntilChanged&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
  &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;value&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="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getValue&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CartItem&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;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&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;// Keep the legacy event working for screens that still listen to it&lt;/span&gt;
&lt;span class="nx"&gt;cartService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cart$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cart:updated&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="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flhfjbusqublef9krghvv.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flhfjbusqublef9krghvv.png" alt="A cart service in the middle holds a BehaviorSubject and exposes cart$ and an add method. On the left, a legacy jQuery header subscribes to cart$ and calls add from a click handler. On the right, a new React checkout page reads cart$ through a small useStream hook and calls add from a button. Both stay in sync because there is only one source of truth" width="800" height="214"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The legacy side subscribes and updates the DOM the same way it does today:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// legacy/header.js (jQuery)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cartService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#cart-badge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#add-to-cart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cartService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;readItemFromForm&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;

&lt;span class="c1"&gt;// Backbone view: tie the subscription to the view's lifetime&lt;/span&gt;
&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cartService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cart$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsubscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="nx"&gt;Backbone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;View&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;React reads the same stream through a small hook. &lt;code&gt;useSyncExternalStore&lt;/code&gt; is the official React API for subscribing to an external store. It also avoids the "tearing" issue that a &lt;code&gt;useEffect&lt;/code&gt; + &lt;code&gt;useState&lt;/code&gt; version can have during concurrent rendering:&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;useSyncExternalStore&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="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Observable&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="s1"&gt;rxjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Subscribe to a stream that always has a current value (like a service backed by a BehaviorSubject)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useStream&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream$&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Observable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;useSyncExternalStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onChange&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stream$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onChange&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsubscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;getValue&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;// In a component&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;CheckoutSummary&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;cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cartService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cart$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cartService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;formatMoney&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the complete seam. A screen can move from the old app to React, and the cart badge in the legacy header will still update, because neither side owns the cart. Once the last legacy subscriber is removed, the service internals can be replaced (with Zustand, TanStack Query or plain React context) without changing any component, because components only depend on &lt;code&gt;cart$&lt;/code&gt; and &lt;code&gt;add&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Issues we faced&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exposing the Subject directly.&lt;/strong&gt; If the service exports &lt;code&gt;state&lt;/code&gt; instead of &lt;code&gt;state.asObservable()&lt;/code&gt;, any file can call &lt;code&gt;.next()&lt;/code&gt;, and we are back to global events. Expose streams for reading and methods for writing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changing emitted objects in place.&lt;/strong&gt; &lt;code&gt;state.value.items.push(item)&lt;/code&gt; changes the object that both apps hold, but nothing is emitted. Always emit a new object, because &lt;code&gt;useSyncExternalStore&lt;/code&gt; depends on reference changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Updates inside updates.&lt;/strong&gt; A legacy handler that calls &lt;code&gt;add()&lt;/code&gt; while handling a &lt;code&gt;cart$&lt;/code&gt; emission can create a loop. Using &lt;code&gt;distinctUntilChanged&lt;/code&gt; on derived streams, and keeping the legacy re-trigger one-way, prevents most of these cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loops with the legacy event.&lt;/strong&gt; If the legacy app both fires and listens to &lt;code&gt;cart:updated&lt;/code&gt;, either wrap it with &lt;code&gt;fromEvent&lt;/code&gt; &lt;em&gt;or&lt;/em&gt; re-trigger it from the service. Do not do both.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Replace Hand-Written Async Code with Operators
&lt;/h2&gt;

&lt;p&gt;Once events become streams, a lot of fragile legacy code can be rewritten in a few lines, and it usually becomes more correct too. A search box is the common example. Most legacy apps have some version of this jQuery 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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lastQuery&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#search&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&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;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;val&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;lastQuery&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;lastQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/search&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="nx"&gt;q&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;done&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;renderResults&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// a slow early response can overwrite a newer one&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks fine, but it has a race condition. If the request for "rea" is slow and the request for "react" is fast, the old results arrive last and replace the correct ones. The RxJS version avoids this by design, because &lt;code&gt;switchMap&lt;/code&gt; unsubscribes from the previous request as soon as a new query comes in:&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;debounceTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;distinctUntilChanged&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;switchMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;catchError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;of&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="s1"&gt;rxjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ajax&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="s1"&gt;rxjs/ajax&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchInput$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;debounceTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nf"&gt;distinctUntilChanged&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nf"&gt;switchMap&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;ajax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getJSON&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SearchResult&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="s2"&gt;`/api/search?q=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&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="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nf"&gt;catchError&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="p"&gt;([])),&lt;/span&gt; &lt;span class="c1"&gt;// one failed search shouldn't kill the stream&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flmfkctbjqouxc9b2d208.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flmfkctbjqouxc9b2d208.png" alt="Marble diagram of a search box. Keystrokes r, re, rea, react on the input line. After debounceTime, only rea and react pass. switchMap starts a request for rea, then cancels it when react arrives, so only the results for react reach the screen. The hand-rolled version below lets the slow rea response arrive last and overwrite the correct results" width="800" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ajax&lt;/code&gt; from &lt;code&gt;rxjs/ajax&lt;/code&gt; cancels the actual HTTP request when it is unsubscribed. With &lt;code&gt;fetch&lt;/code&gt;, pass an &lt;code&gt;AbortSignal&lt;/code&gt; inside &lt;code&gt;new Observable&lt;/code&gt; (or use &lt;code&gt;fromFetch&lt;/code&gt; from &lt;code&gt;rxjs/fetch&lt;/code&gt;) to get the same behavior.&lt;/p&gt;

&lt;p&gt;The same small set of operators covers most of the other hand-written patterns we see in legacy apps:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Operator&lt;/th&gt;
&lt;th&gt;Why it is better than the legacy version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Only the latest request matters (search, filters, route params)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;switchMap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cancels old requests, so results never arrive out of order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ignore clicks while a save is in flight&lt;/td&gt;
&lt;td&gt;&lt;code&gt;exhaustMap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Replaces &lt;code&gt;isSaving&lt;/code&gt; flags and disabled-button workarounds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every action must run, in order (queued writes)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;concatMap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Writes do not mix, and no update is lost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Poll until the user leaves the page&lt;/td&gt;
&lt;td&gt;&lt;code&gt;timer(0, 5000).pipe(switchMap(load), takeUntil(leave$))&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Polls never overlap, and polling stops reliably&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry flaky calls with backoff&lt;/td&gt;
&lt;td&gt;&lt;code&gt;retry({ count: 3, delay: (_, i) =&amp;gt; timer(2 ** i * 500) })&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Limited attempts with a growing delay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Combine several sources&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;combineLatest&lt;/code&gt;, &lt;code&gt;withLatestFrom&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Replaces flags like &lt;code&gt;if (userLoaded &amp;amp;&amp;amp; settingsLoaded)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time out a slow call&lt;/td&gt;
&lt;td&gt;&lt;code&gt;timeout({ first: 8000 })&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Configured in one place, and errors are reported properly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Polling needs one more example, because legacy polling code often causes duplicate requests and battery drain:&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;timer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;switchMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;takeUntil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;share&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fromEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filter&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="s1"&gt;rxjs&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;hidden$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fromEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;visibilitychange&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jobStatus$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jobId&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="nf"&gt;timer&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="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;switchMap&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;))),&lt;/span&gt;
    &lt;span class="nf"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;delay&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="nf"&gt;takeUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hidden$&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;share&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;// several widgets can watch the same job with one poller&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Leaked subscriptions
&lt;/h3&gt;

&lt;p&gt;Every &lt;code&gt;subscribe()&lt;/code&gt; must be closed at some point. In legacy code, connect it to the lifecycle that already exists: a Backbone view's &lt;code&gt;remove&lt;/code&gt;, an Ember component's &lt;code&gt;willDestroy&lt;/code&gt;, an AngularJS &lt;code&gt;$scope.$on('$destroy')&lt;/code&gt;, or a jQuery plugin's &lt;code&gt;destroy&lt;/code&gt;. In React, the hook's cleanup takes care of it. For streams that should end on an event, keep &lt;code&gt;takeUntil(destroy$)&lt;/code&gt; as the &lt;strong&gt;last&lt;/strong&gt; operator in the pipe, so that nothing after it can subscribe again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hot and cold Observables
&lt;/h3&gt;

&lt;p&gt;A cold Observable (like &lt;code&gt;defer(() =&amp;gt; fetch(...))&lt;/code&gt;) runs again for every subscriber. If three components subscribe to the same stream, three requests are made. When a result should be shared, add &lt;code&gt;share()&lt;/code&gt; or &lt;code&gt;shareReplay({ bufferSize: 1, refCount: true })&lt;/code&gt;. Use &lt;code&gt;refCount: true&lt;/code&gt; unless the source really needs to run forever. Without it, &lt;code&gt;shareReplay&lt;/code&gt; keeps the inner subscription (and its polling or socket) running even after the last subscriber has left.&lt;/p&gt;

&lt;h3&gt;
  
  
  Turning everything into a stream
&lt;/h3&gt;

&lt;p&gt;A common mistake in RxJS projects is a codebase where a simple boolean flag becomes a &lt;code&gt;BehaviorSubject&amp;lt;boolean&amp;gt;&lt;/code&gt; passed through four operators. &lt;strong&gt;Use streams only where time matters&lt;/strong&gt;: events, sockets, polling, typeahead and state shared between the two apps. For normal server data in React (load a list, show it, refetch on change), &lt;a href="https://tanstack.com/query/latest" rel="noopener noreferrer"&gt;TanStack Query&lt;/a&gt; is simpler and also handles caching. For local component state, use &lt;code&gt;useState&lt;/code&gt;. A simple rule: if you cannot draw a marble diagram for it, it probably should not be an Observable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Errors stop streams
&lt;/h3&gt;

&lt;p&gt;When an error reaches a long-running stream, the stream ends and every subscriber stops getting updates, without any visible message. Catch errors &lt;strong&gt;inside&lt;/strong&gt; the &lt;code&gt;switchMap&lt;/code&gt;/&lt;code&gt;mergeMap&lt;/code&gt; (as shown in the search example) so that the outer stream keeps running, and log every error you catch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bundle size and RxJS versions
&lt;/h3&gt;

&lt;p&gt;RxJS 7 supports tree-shaking, and from version 7.2 the operators are exported directly from &lt;code&gt;rxjs&lt;/code&gt;. So a typical bridge layer adds only a small amount to the bundle. If the legacy app still uses RxJS 5 or 6 (common in older Angular apps), the &lt;a href="https://rxjs.dev/deprecations/breaking-changes" rel="noopener noreferrer"&gt;official migration guide&lt;/a&gt; explains the upgrade to 7. &lt;code&gt;rxjs-compat&lt;/code&gt; was meant only for the 5 to 6 upgrade and should not be used in a new layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the Bridge
&lt;/h2&gt;

&lt;p&gt;Streams are easy to test without a browser. &lt;code&gt;TestScheduler&lt;/code&gt; runs time-based operators on virtual time, so a 300 ms debounce does not actually wait 300 ms. Marble strings describe the input and the expected output:&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;TestScheduler&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="s1"&gt;rxjs/testing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// The search stream, with its inputs passed in instead of read from globals&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;searchPipeline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input$&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Observable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&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="nx"&gt;Observable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SearchResult&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="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;input$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;debounceTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;distinctUntilChanged&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;switchMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;search&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;scheduler&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;TestScheduler&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;search only queries the last term after a pause&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;cold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expectObservable&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a 100ms b 400ms c|&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;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&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;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;q&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="nf"&gt;cold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--r|&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;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`result for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;q&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;// 'rea' is dropped by the debounce; 'react' settles at 401ms, its result arrives 2 frames later&lt;/span&gt;
    &lt;span class="nf"&gt;expectObservable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;searchPipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;403ms x 99ms |&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;x&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="s1"&gt;result for react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Writing the pipeline as a function of its inputs is what makes it testable, and it is a good practice for the seam in any case. For checking that both apps behave the same, use the approach from our &lt;a href="https://cobuildx.ai/blog/jquery-to-react-migration-guide" rel="noopener noreferrer"&gt;migration guides&lt;/a&gt;: one end-to-end test suite that runs the same screen in the old app and in the new app, and checks for the same visible result. The stream layer is what keeps those results the same, so it needs good test coverage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Temporary Bridge or Long-Term Layer?
&lt;/h2&gt;

&lt;p&gt;At the end of the migration, decide what happens to the RxJS layer. Both options below are fine, as long as the team makes a clear decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Temporary bridge.&lt;/strong&gt; The bridge was added so that old and new code could share state. Once the legacy app is removed, plain React state, TanStack Query or a small store can handle most of it. Remove the Subjects, and keep only the few streams that represent real events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-term layer.&lt;/strong&gt; Apps with a lot of real-time behavior (trading screens, live dashboards using sockets, collaborative editors, IoT consoles) often keep RxJS as the event layer under React. In that case, document the rules: where streams are kept, how errors are handled, and which shared streams use &lt;code&gt;shareReplay&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faklt9chrr7c2wc97u2jm.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faklt9chrr7c2wc97u2jm.png" alt="Checklist for retiring the RxJS bridge. Every legacy subscriber removed. Legacy event re-triggers deleted. Each service's Subject either replaced by a React-native store or kept and documented as a real event stream. No fromEventPattern wrappers left pointing at legacy objects. Marble tests moved to the new implementation or deleted with it" width="800" height="220"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Whichever option you choose, delete the &lt;code&gt;fromEventPattern&lt;/code&gt; wrappers around legacy objects in the same sprint as the last legacy screen. These wrappers are always temporary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further in This Series
&lt;/h2&gt;

&lt;p&gt;This post covers one technique within a larger migration plan. The posts below explain the overall plan and each starting framework in more detail:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://cobuildx.ai/blog/frontend-migration-strategies" rel="noopener noreferrer"&gt;Frontend migration strategies&lt;/a&gt;: strangler fig, branch by abstraction and vertical slicing, and how they combine.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cobuildx.ai/blog/one-way-two-way-binding" rel="noopener noreferrer"&gt;One-way vs two-way data binding&lt;/a&gt;: why observers and watchers are difficult to migrate, and what replaces them.&lt;/li&gt;
&lt;li&gt;Framework guides: &lt;a href="https://cobuildx.ai/blog/angular-to-react-migration-guide" rel="noopener noreferrer"&gt;Angular&lt;/a&gt;, &lt;a href="https://cobuildx.ai/blog/ember-to-react-migration-guide" rel="noopener noreferrer"&gt;Ember&lt;/a&gt;, &lt;a href="https://cobuildx.ai/blog/backbone-to-react-migration-guide" rel="noopener noreferrer"&gt;Backbone&lt;/a&gt;, &lt;a href="https://cobuildx.ai/blog/jquery-to-react-migration-guide" rel="noopener noreferrer"&gt;jQuery&lt;/a&gt; and &lt;a href="https://cobuildx.ai/blog/vanilla-js-to-react-migration-guide" rel="noopener noreferrer"&gt;vanilla JS&lt;/a&gt; to React.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Our Migration Plugins
&lt;/h2&gt;

&lt;p&gt;Finding every event source, observer and shared service in a legacy app takes the most time, and it decides where the streams should go. We have packaged our approach as free, open-source plugins for Claude Code, one for each starting framework:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plugin&lt;/th&gt;
&lt;th&gt;Migrates from&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/cobuild-tech/cbx-plugins/tree/main/ember-to-react" rel="noopener noreferrer"&gt;&lt;code&gt;ember-to-react&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Ember Classic and Octane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/cobuild-tech/cbx-plugins/tree/main/angular-to-react" rel="noopener noreferrer"&gt;&lt;code&gt;angular-to-react&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Angular 2+ and AngularJS 1.x, including hybrid ngUpgrade apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/cobuild-tech/cbx-plugins/tree/main/backbone-to-react" rel="noopener noreferrer"&gt;&lt;code&gt;backbone-to-react&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Backbone.js, Marionette, jQuery templates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/cobuild-tech/cbx-plugins/tree/main/jquery-to-react" rel="noopener noreferrer"&gt;&lt;code&gt;jquery-to-react&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;jQuery apps, plugins and jQuery UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/cobuild-tech/cbx-plugins/tree/main/vanillajs-to-react" rel="noopener noreferrer"&gt;&lt;code&gt;vanillajs-to-react&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Plain HTML, CSS and JavaScript&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each plugin comes with a migration skill, a &lt;code&gt;/&amp;lt;plugin&amp;gt;:plan&lt;/code&gt; command that writes a migration plan (strategy, units, route map, test plan and risks) without touching your code, a read-only inventory agent that maps the old app, a parity-reviewer agent that compares each migrated piece with its original, and a bundled Context7 server for up-to-date library docs. To install one in Claude 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="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt; &lt;span class="nx"&gt;marketplace&lt;/span&gt; &lt;span class="nx"&gt;add&lt;/span&gt; &lt;span class="nx"&gt;cobuild&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;tech&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;cbx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;plugins&lt;/span&gt;
&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt; &lt;span class="nx"&gt;install&lt;/span&gt; &lt;span class="nx"&gt;backbone&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;react&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;cbx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;plugins&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They also work in Cursor, in VS Code with GitHub Copilot, and in the GitHub Copilot CLI. Progress is kept in a &lt;code&gt;.migration/&lt;/code&gt; folder in your repo, so a migration can carry on across sessions and teammates. The source and setup steps are at &lt;a href="https://github.com/cobuild-tech/cbx-plugins" rel="noopener noreferrer"&gt;cobuild-tech/cbx-plugins&lt;/a&gt;, and issues are welcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  About MigrateX
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxywolgtbstuveflre5yv.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxywolgtbstuveflre5yv.png" alt="MigrateX" width="799" height="293"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MigrateX is our platform and service for moving software, data and infrastructure from one technology to another. It combines automation with experienced engineers. The automation does the repetitive work quickly, and the engineers make the tricky decisions, like which shared state needs a stream-based seam and which can be moved directly.&lt;/p&gt;

&lt;p&gt;A MigrateX project usually runs like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Assessment.&lt;/strong&gt; We map every route, shared service, event source and hidden dependency in your app, so you see where the work is before anyone agrees on a timeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan.&lt;/strong&gt; With your team, we choose the strategy, the slice order, and what can simply be deleted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration, step by step.&lt;/strong&gt; Automation handles the repetitive parts. Our engineers handle the router, the seams and the code that connects old and new.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proof that it works.&lt;/strong&gt; The same tests run against both versions. A piece only counts as moved when it passes on both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handover.&lt;/strong&gt; We remove the temporary code, the seams and the flags, and leave your team with a codebase they understand and own.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your app keeps running and shipping features the whole time, so there's no big launch day. Our &lt;a href="https://github.com/cobuild-tech/migratex-examples" rel="noopener noreferrer"&gt;migratex-examples&lt;/a&gt; repo has small apps in each legacy framework next to their migrated versions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Working on the async code of a legacy app? Share a few details with us through &lt;a href="https://cobuildx.ai/contact" rel="noopener noreferrer"&gt;Contact Us&lt;/a&gt;, and we will start with a free discussion on where the seams should go.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;RxJS&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RxJS. &lt;em&gt;Overview&lt;/em&gt; and API reference. &lt;a href="https://rxjs.dev/guide/overview" rel="noopener noreferrer"&gt;https://rxjs.dev/guide/overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RxJS. &lt;em&gt;fromEventPattern&lt;/em&gt;. &lt;a href="https://rxjs.dev/api/index/function/fromEventPattern" rel="noopener noreferrer"&gt;https://rxjs.dev/api/index/function/fromEventPattern&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RxJS. &lt;em&gt;Testing RxJS Code with Marble Diagrams&lt;/em&gt;. &lt;a href="https://rxjs.dev/guide/testing/marble-testing" rel="noopener noreferrer"&gt;https://rxjs.dev/guide/testing/marble-testing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RxJS. &lt;em&gt;Breaking Changes in Version 7&lt;/em&gt;. &lt;a href="https://rxjs.dev/deprecations/breaking-changes" rel="noopener noreferrer"&gt;https://rxjs.dev/deprecations/breaking-changes&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Ben Lesh. &lt;em&gt;Hot vs Cold Observables&lt;/em&gt;. &lt;a href="https://benlesh.medium.com/hot-vs-cold-observables-f8094ed53339" rel="noopener noreferrer"&gt;https://benlesh.medium.com/hot-vs-cold-observables-f8094ed53339&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;React and migration patterns&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React. &lt;em&gt;useSyncExternalStore&lt;/em&gt;. &lt;a href="https://react.dev/reference/react/useSyncExternalStore" rel="noopener noreferrer"&gt;https://react.dev/reference/react/useSyncExternalStore&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Martin Fowler. &lt;em&gt;Branch By Abstraction&lt;/em&gt;. &lt;a href="https://martinfowler.com/bliki/BranchByAbstraction.html" rel="noopener noreferrer"&gt;https://martinfowler.com/bliki/BranchByAbstraction.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Microsoft. &lt;em&gt;Anti-corruption Layer pattern&lt;/em&gt;. &lt;a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;TanStack Query. &lt;em&gt;Overview&lt;/em&gt;. &lt;a href="https://tanstack.com/query/latest/docs/framework/react/overview" rel="noopener noreferrer"&gt;https://tanstack.com/query/latest/docs/framework/react/overview&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Source code&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CobuildX AI. &lt;em&gt;cbx-plugins&lt;/em&gt;: Claude Code plugins for Backbone, Angular, Ember, jQuery and Vanilla JS to React migrations. &lt;a href="https://github.com/cobuild-tech/cbx-plugins" rel="noopener noreferrer"&gt;https://github.com/cobuild-tech/cbx-plugins&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CobuildX AI. &lt;em&gt;migratex-examples&lt;/em&gt;: small jQuery, Backbone, Angular, Ember and Vanilla JS apps and their migrated versions. &lt;a href="https://github.com/cobuild-tech/migratex-examples" rel="noopener noreferrer"&gt;https://github.com/cobuild-tech/migratex-examples&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cobuildx.ai/blog/modernizing-legacy-apps-with-rxjs" rel="noopener noreferrer"&gt;CobuildX blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rxjs</category>
      <category>javascript</category>
      <category>react</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Strangler Fig, Branch by Abstraction, Vertical Slices: Choosing a Frontend Migration Strategy</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Mon, 12 Oct 2026 05:31:32 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/strangler-fig-branch-by-abstraction-vertical-slices-choosing-a-frontend-migration-strategy-39kp</link>
      <guid>https://dev.to/cobuildx-ai/strangler-fig-branch-by-abstraction-vertical-slices-choosing-a-frontend-migration-strategy-39kp</guid>
      <description>&lt;p&gt;&lt;em&gt;Strangler fig, branch by abstraction, vertical slicing: the strategy decides whether a frontend migration ships, more than the framework you move to. Here's how each one works, where each one bites, and how we combine them on real projects.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Teams that plan a frontend migration spend weeks on the target: React or Vue, Next.js or Vite, Redux or TanStack Query. Then they spend almost no time on the part that decides whether it ships: &lt;strong&gt;how the old app and the new app will live together until the old one is gone.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That part is the migration strategy. Pick the wrong one and a good framework choice still turns into an 18-month rewrite branch that never merges. Pick the right one and even a messy legacy app moves over a piece at a time, with users never noticing a launch day.&lt;/p&gt;

&lt;p&gt;This post covers the strategies we actually use: the &lt;strong&gt;big-bang rewrite&lt;/strong&gt; (as the baseline), the &lt;strong&gt;strangler fig&lt;/strong&gt;, &lt;strong&gt;branch by abstraction&lt;/strong&gt;, and &lt;strong&gt;vertical slicing&lt;/strong&gt;, plus the supporting tactics that make them safe: parallel runs, feature flags, micro-frontends, anti-corruption layers and expand-contract. Then it shows how they fit together, because on a real project you rarely use just one. Our framework-specific guides (&lt;a href="https://cobuildx.ai/blog/angular-to-react-migration-guide" rel="noopener noreferrer"&gt;Angular&lt;/a&gt;, &lt;a href="https://cobuildx.ai/blog/ember-to-react-migration-guide" rel="noopener noreferrer"&gt;Ember&lt;/a&gt;, &lt;a href="https://cobuildx.ai/blog/backbone-to-react-migration-guide" rel="noopener noreferrer"&gt;Backbone&lt;/a&gt;, &lt;a href="https://cobuildx.ai/blog/jquery-to-react-migration-guide" rel="noopener noreferrer"&gt;jQuery&lt;/a&gt;, &lt;a href="https://cobuildx.ai/blog/vanilla-js-to-react-migration-guide" rel="noopener noreferrer"&gt;vanilla JS&lt;/a&gt;) go deeper on each starting point. This one is about the strategy underneath all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Short Version
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Unit of change&lt;/th&gt;
&lt;th&gt;What runs side by side&lt;/th&gt;
&lt;th&gt;Rollback&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Big-bang rewrite&lt;/td&gt;
&lt;td&gt;The whole app&lt;/td&gt;
&lt;td&gt;Nothing in production until launch day&lt;/td&gt;
&lt;td&gt;Hard: the old app has gone stale&lt;/td&gt;
&lt;td&gt;Small or frozen apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strangler fig&lt;/td&gt;
&lt;td&gt;A route or a screen&lt;/td&gt;
&lt;td&gt;Old and new apps behind one router&lt;/td&gt;
&lt;td&gt;Point the route back at the old app&lt;/td&gt;
&lt;td&gt;Apps with clear routes or pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Branch by abstraction&lt;/td&gt;
&lt;td&gt;A shared dependency behind a seam&lt;/td&gt;
&lt;td&gt;Old and new implementations behind one interface&lt;/td&gt;
&lt;td&gt;Flip the switch back&lt;/td&gt;
&lt;td&gt;Services, stores and widgets used everywhere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vertical slicing&lt;/td&gt;
&lt;td&gt;One user-facing feature, top to bottom&lt;/td&gt;
&lt;td&gt;Migrated and unmigrated features&lt;/td&gt;
&lt;td&gt;Per slice&lt;/td&gt;
&lt;td&gt;Choosing the order of work&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first row is the one people start with. The other three are what actually works, and they answer different questions. &lt;strong&gt;Strangler fig decides where the line between old and new runs. Branch by abstraction handles the things that cross that line. Vertical slicing decides what to move next.&lt;/strong&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F68byzglixxoy7j0cr11g.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F68byzglixxoy7j0cr11g.png" alt="Four strategies side by side. Big-bang: the old app runs until a single cut-over day. Strangler fig: a router sends some routes to the new app and the rest to the old one. Branch by abstraction: callers talk to an interface with an old and a new implementation behind it. Vertical slicing: one feature is moved through UI, state, API and tests at once" width="800" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Big-Bang Rewrite: the Baseline
&lt;/h2&gt;

&lt;p&gt;The big-bang rewrite is the default plan nobody writes down: start a new repo, rebuild everything, switch over on a chosen day. It's worth understanding properly, because every other strategy exists to avoid its failure modes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When it is actually right&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The app is small.&lt;/strong&gt; A few screens that one or two people can rebuild in a few weeks. The overhead of running two apps side by side costs more than the rewrite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The app is frozen.&lt;/strong&gt; No new features are planned, so the old app won't move while you rebuild it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The old app can't host anything new.&lt;/strong&gt; Some legacy stacks make it so painful to mount new code that coexistence costs more than starting clean. This is rarer than it seems. Almost anything can serve an iframe or hand off a route.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why it usually fails&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The target keeps moving.&lt;/strong&gt; The business doesn't stop for your rewrite. Every feature shipped in the old app is one more thing the new one has to catch up on, and the gap rarely closes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing is proven until the end.&lt;/strong&gt; Months of work with zero real users means every wrong assumption surfaces on launch day, all at once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The old app holds knowledge nobody wrote down.&lt;/strong&gt; Ten years of bug fixes are hidden in it. Joel Spolsky called rewriting from scratch &lt;a href="https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/" rel="noopener noreferrer"&gt;the single worst strategic mistake&lt;/a&gt; a software company can make, for exactly this reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback is fiction.&lt;/strong&gt; By launch day the old app is months behind. "Roll back" means going back to software everyone has stopped maintaining.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The incremental strategies below all share one idea: &lt;strong&gt;get the new code in front of real users early, in small pieces, while the old app keeps running.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Strangler Fig
&lt;/h2&gt;

&lt;p&gt;Martin Fowler &lt;a href="https://martinfowler.com/bliki/StranglerFigApplication.html" rel="noopener noreferrer"&gt;named this pattern&lt;/a&gt; after a vine that grows around a host tree until the tree is gone and the vine stands on its own. In a frontend migration, the vine is the new app and the tree is the old one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Put something in front of both apps.&lt;/strong&gt; A reverse proxy, a CDN rule, or a thin shell app that owns the router. Every request goes through it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send every route to the old app at first.&lt;/strong&gt; Nothing has changed for users, but the switch now exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move one route.&lt;/strong&gt; Build it in the new app and flip that one path in the router.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeat&lt;/strong&gt; until the old app serves nothing. Then switch it off.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The routing layer can be as simple as a list of migrated paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// shell/router.ts: which app owns which path&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MIGRATED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/settings&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="s1"&gt;/profile/:username&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="s1"&gt;/article/:slug&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ownerOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;legacy&lt;/span&gt;&lt;span class="dl"&gt;'&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;MIGRATED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;matchPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&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="s1"&gt;react&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="s1"&gt;legacy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production this is often a CDN or proxy rule instead of code, and each route can sit behind a feature flag so you can roll it out to 5% of users first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Route-level or widget-level&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There are two grains to strangle at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Route-level.&lt;/strong&gt; Whole pages move at once. It's the cleanest option: each page is owned by one framework, and the boundary is a URL. Start here if your app has real routes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Widget-level.&lt;/strong&gt; New components are mounted inside old pages (a React date picker inside an Ember template, for example). This is how you migrate apps with one giant page, or a shared header that appears everywhere. It's more fragile, because the two frameworks now share a DOM.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most projects use both: widget-level for the shared shell (nav, header, notifications), route-level for everything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it shines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real users see real progress within weeks.&lt;/strong&gt; Every migrated route is in production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback is one line.&lt;/strong&gt; Point the route back at the old app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature work never stops.&lt;/strong&gt; New features go straight into the new app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The end is visible.&lt;/strong&gt; "34 of 52 routes migrated" is a progress bar a manager can read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where it costs you&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two runtimes at once.&lt;/strong&gt; Users may download both frameworks for a while. Keep the old bundle off migrated pages where you can.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared state across the boundary.&lt;/strong&gt; Login, the current user, feature flags and unsaved drafts have to be readable by both apps. Put them somewhere neutral: a cookie, &lt;code&gt;localStorage&lt;/code&gt;, or the API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSS bleed.&lt;/strong&gt; Global styles from one app leak into the other. Scope the new app's styles (CSS Modules, a prefix, or Shadow DOM) before you mount anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Navigation between apps.&lt;/strong&gt; Crossing from a React page to an Ember page is a full page load unless the shell handles it. Users notice this, so group related routes and move them together.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Gotchas we hit&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The router is the hard part, not the pages.&lt;/strong&gt; Agree on how deep links, redirects, 404s and query strings behave across both apps before moving a single route.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth needs to move first.&lt;/strong&gt; If the new app can't read the old app's session, every migrated page looks logged out. A shared cookie is the usual fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't strangle by file.&lt;/strong&gt; Moving "all the components in &lt;code&gt;/shared&lt;/code&gt;" isn't strangling, it's a horizontal layer (see vertical slicing below). Strangle by what users see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete as you go.&lt;/strong&gt; Once a route is moved and stable, remove its old code. Otherwise the old app never shrinks and you end up maintaining both forever.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;a href="https://cobuildx.ai/blog/angular-to-react-migration-guide" rel="noopener noreferrer"&gt;Angular guide&lt;/a&gt; walks through a strangler fig migration end to end, including the shell app and the staged rollout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Branch by Abstraction
&lt;/h2&gt;

&lt;p&gt;Strangler fig works when the thing you're replacing has a clear boundary, like a URL. But some parts of a frontend don't have one. The data layer, the auth service, the API client, the store, a date library: they're used from every page. You can't route around them. &lt;strong&gt;Branch by abstraction&lt;/strong&gt; is the pattern for those.&lt;/p&gt;

&lt;p&gt;The name comes from Paul Hammant and is described by &lt;a href="https://martinfowler.com/bliki/BranchByAbstraction.html" rel="noopener noreferrer"&gt;Fowler&lt;/a&gt; and &lt;a href="https://trunkbaseddevelopment.com/branch-by-abstraction/" rel="noopener noreferrer"&gt;trunkbaseddevelopment.com&lt;/a&gt;. The idea: instead of making a long-lived version-control branch to replace something, you make the "branch" in the code itself, behind an interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add a seam.&lt;/strong&gt; Write an interface that describes what callers need. Make the old implementation fit it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move callers onto the seam.&lt;/strong&gt; One at a time, change code that talks to the old thing directly so it talks to the interface instead. Ship each change. Nothing behaves differently yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build the new implementation&lt;/strong&gt; behind the same interface, in the main branch, switched off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Switch over.&lt;/strong&gt; A flag picks the new implementation, first for internal users, then for everyone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete the old implementation&lt;/strong&gt;, and the flag. If the seam isn't useful anymore, delete it too.&lt;/li&gt;
&lt;/ol&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7grlce5rhk3i85n27vjr.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7grlce5rhk3i85n27vjr.png" alt="Branch by abstraction in four steps. One: callers use the old implementation directly. Two: callers use an interface, still backed by the old implementation. Three: a new implementation is built behind the same interface and a flag picks between them. Four: the old implementation and flag are deleted" width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's what that looks like for a data layer. Say pages load articles through an Ember Data store or Backbone models, and you're replacing that with a plain API client and TanStack Query. The seam is a small repository interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// articles/repository.ts: the seam&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ArticleRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Article&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ArticleFilter&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Article&lt;/span&gt;&lt;span class="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="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ArticleInput&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Article&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// old: wraps the legacy store, converting its records to plain objects&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;legacyArticles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ArticleRepository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;get&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;slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;toArticle&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;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queryRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;article&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="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
  &lt;span class="na"&gt;list&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;filter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;article&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toArticle&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;save&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;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;toArticle&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;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;article&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// new: talks to the API directly&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiArticles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ArticleRepository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/articles/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/articles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;articles&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/articles&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;article&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;articles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ArticleRepository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isOn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;articles-api&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="nx"&gt;apiArticles&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;legacyArticles&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details make this work. First, &lt;strong&gt;the interface speaks plain objects, not framework records.&lt;/strong&gt; If callers receive Ember Data models, they'll keep calling &lt;code&gt;.get('title')&lt;/code&gt; and you haven't built a seam, just a wrapper. Second, &lt;strong&gt;both implementations live in the main branch&lt;/strong&gt; and are tested in CI. The "branch" is a flag, not a Git branch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where you'll use it in a frontend&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The data layer.&lt;/strong&gt; Ember Data, Backbone collections, Angular services with their own caches → a query library. This is the most common use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth and session.&lt;/strong&gt; One &lt;code&gt;session&lt;/code&gt; interface that both the old and new apps call, so login works on both sides of the strangler boundary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A shared component.&lt;/strong&gt; The date picker, the rich text editor, the data grid. Wrap it in one component API, then swap the implementation underneath.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A library replacement&lt;/strong&gt; that isn't a framework change at all: moment.js → date-fns, a charting library, an HTTP client.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why not just use a long-lived branch?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because a migration branch that lives for months is a big-bang rewrite in disguise. It drifts from main every day, the merge at the end is huge, and nothing in it has met a real user. Branch by abstraction keeps every step small and merged. It pairs naturally with &lt;a href="https://trunkbaseddevelopment.com/" rel="noopener noreferrer"&gt;trunk-based development&lt;/a&gt; and with &lt;a href="https://martinfowler.com/articles/feature-toggles.html" rel="noopener noreferrer"&gt;feature toggles&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cost:&lt;/strong&gt; extra code for a while (the interface, two implementations and a flag), and the discipline to actually do step five. Seams and flags that outlive the migration become their own kind of legacy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vertical Slicing
&lt;/h2&gt;

&lt;p&gt;Strangler fig and branch by abstraction tell you &lt;em&gt;how&lt;/em&gt; to keep old and new running together. &lt;strong&gt;Vertical slicing tells you what to move next.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A vertical slice is one user-facing feature moved &lt;strong&gt;top to bottom&lt;/strong&gt;: its UI, its state, its data access, its routes and its tests, all together, until a user can do that thing entirely in the new code. The opposite is a horizontal layer: "first we'll port all the models, then all the services, then all the components".&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3cgvea344g4w4hfn9m62.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3cgvea344g4w4hfn9m62.png" alt="Grid of features (login, feed, article, editor, settings) against layers (UI, state, data access, tests). A horizontal plan fills one whole layer at a time and no feature is finished until the end. A vertical plan fills one whole feature column at a time, and each finished column ships" width="799" height="298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why horizontal layers fail&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nothing ships until the last layer is done.&lt;/strong&gt; Porting every model first gives you months of code with no user, which is the big-bang problem again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You guess at requirements.&lt;/strong&gt; Without a real screen using it, a ported service is shaped by what the old code did, not what the new UI needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Progress is invisible.&lt;/strong&gt; "80% of the services are done" means nothing to the people paying for it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Choosing the first slice&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first slice is mostly about proving the plumbing: routing between apps, shared auth, the build and deploy, the test setup. So pick one that is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real, but low-risk.&lt;/strong&gt; A settings page or a profile page, not checkout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Light on shared dependencies.&lt;/strong&gt; It shouldn't need half the app's services to render.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy to compare.&lt;/strong&gt; It has clear behavior you can test old against new.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Used often enough&lt;/strong&gt; that you'll hear quickly if something's wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After the first one, order the rest by &lt;strong&gt;value against coupling.&lt;/strong&gt; Move high-value, loosely coupled features early. Leave the tangled core, the screen that touches everything, until the seams around it already exist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How slices map onto the other strategies&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In a well-run migration, a vertical slice usually &lt;strong&gt;is&lt;/strong&gt; a strangler fig step: a feature is a set of routes, and moving the feature means flipping those routes. When a slice hits a dependency it shares with unmigrated features (the article store used by both the feed and the editor), that's where a branch-by-abstraction seam goes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Supporting Tactics
&lt;/h2&gt;

&lt;p&gt;These aren't migration strategies on their own. They're tools that make the three above safe.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parallel run and dark launching
&lt;/h3&gt;

&lt;p&gt;Run the new code alongside the old one in production, &lt;strong&gt;compare the results, but only show the old one.&lt;/strong&gt; GitHub's &lt;a href="https://github.com/github/scientist" rel="noopener noreferrer"&gt;Scientist&lt;/a&gt; library made this famous for backend code. In a frontend it's useful for a new data layer or a new calculation (pricing, permissions, validation): call both implementations behind the seam, log any differences, return the old result. Once the differences hit zero, switch. &lt;a href="https://martinfowler.com/bliki/DarkLaunching.html" rel="noopener noreferrer"&gt;Dark launching&lt;/a&gt; is the same idea for whole features: ship them to production hidden, and turn them on later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// compare old and new behind the seam; users still get the old result&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;oldResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newResult&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allSettled&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nx"&gt;legacyArticles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;apiArticles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="nf"&gt;reportDiff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;articles.get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;oldResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newResult&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oldResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rejected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;oldResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&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;oldResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch the cost: a parallel run doubles API calls. Sample a percentage of traffic, and never run both sides of a write.&lt;/p&gt;

&lt;h3&gt;
  
  
  Feature flags and canary releases
&lt;/h3&gt;

&lt;p&gt;Every switch in this post (a migrated route, a new implementation behind a seam) should be a flag you can flip without a deploy. That lets you release to staff first, then 5% of users, then everyone, and roll back in seconds. Name each flag after the migration step and give it an expiry date. The &lt;a href="https://martinfowler.com/articles/feature-toggles.html" rel="noopener noreferrer"&gt;feature toggles article&lt;/a&gt; on Fowler's site is the best guide to keeping them under control.&lt;/p&gt;

&lt;h3&gt;
  
  
  Micro-frontends as a way to coexist
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://martinfowler.com/articles/micro-frontends.html" rel="noopener noreferrer"&gt;Micro-frontends&lt;/a&gt; (module federation, single-spa, web components) let several apps share one page. They're a useful &lt;strong&gt;vehicle&lt;/strong&gt; for widget-level strangling: the old app becomes one micro-frontend, the new one another. But think twice before making them the &lt;strong&gt;destination&lt;/strong&gt;. If the goal is one React app, use the micro-frontend setup as scaffolding and take it down when the migration ends. Otherwise you've swapped one legacy app for a distributed system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anti-corruption layer
&lt;/h3&gt;

&lt;p&gt;An &lt;a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer" rel="noopener noreferrer"&gt;anti-corruption layer&lt;/a&gt; translates between the old model and the new one so the old model's quirks don't leak into new code. The &lt;code&gt;toArticle()&lt;/code&gt; function in the branch-by-abstraction example is a tiny one: it turns a legacy record (with its computed properties, observers and odd field names) into a plain object the new code can trust. Keep these at the boundary and delete them when the old side is gone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Expand and contract
&lt;/h3&gt;

&lt;p&gt;Also called &lt;a href="https://martinfowler.com/bliki/ParallelChange.html" rel="noopener noreferrer"&gt;parallel change&lt;/a&gt;. When old and new code share data (an API response, a &lt;code&gt;localStorage&lt;/code&gt; key, a URL format, a cookie), you can't change its shape in one go. &lt;strong&gt;Expand&lt;/strong&gt; first: support both the old and new shapes. &lt;strong&gt;Migrate&lt;/strong&gt; each reader and writer. &lt;strong&gt;Contract&lt;/strong&gt;: remove the old shape once nothing uses it. It's how you rename a query parameter or restructure saved user preferences without breaking the half of the app that hasn't moved yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combining Them: the Playbook We Use
&lt;/h2&gt;

&lt;p&gt;On a real project these aren't alternatives. They are layers of one plan:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Answered by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where does the line between old and new run?&lt;/td&gt;
&lt;td&gt;Strangler fig (routes, then widgets)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What do we move next?&lt;/td&gt;
&lt;td&gt;Vertical slicing (value against coupling)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What about things every page depends on?&lt;/td&gt;
&lt;td&gt;Branch by abstraction (a seam, two implementations, a flag)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How do we know the new one behaves the same?&lt;/td&gt;
&lt;td&gt;Parallel run and shared tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How do we release and roll back?&lt;/td&gt;
&lt;td&gt;Feature flags and canaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How do we change shared data safely?&lt;/td&gt;
&lt;td&gt;Expand and contract&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgha0thibr5uojqtheaj4.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgha0thibr5uojqtheaj4.png" alt="Timeline of a combined migration in four phases. Foundation: shell router, shared auth, seams around the data layer. First slice: one low-risk feature moved end to end. Steady state: slices move route by route behind flags while seams switch to new implementations. Removal: the old app, the seams and the flags are deleted" width="800" height="292"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In phases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Foundation.&lt;/strong&gt; Put the shell or proxy in front of the old app, with every route still pointing at it. Make auth work on both sides. Set up one test suite that can run against either app. Add seams around the data layer and other shared services. Nothing changes for users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First slice.&lt;/strong&gt; Move one low-risk feature end to end. Its real job is to prove the plumbing: routing, auth, deploys, monitoring, rollback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steady state.&lt;/strong&gt; Move slices in order of value against coupling. Each slice is a set of routes flipped behind a flag. Switch seams to their new implementations as soon as their callers allow it, with a parallel run where correctness matters. Ship new features only in the new app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Removal.&lt;/strong&gt; When the old app serves no routes, switch it off. Then delete the seams you no longer need, the anti-corruption layers and every migration flag. A migration isn't done until the scaffolding is gone.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Choosing a Strategy
&lt;/h2&gt;

&lt;p&gt;If you're deciding where to start, these questions settle most of it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If your app…&lt;/th&gt;
&lt;th&gt;Lean towards&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Is a handful of screens, or is frozen&lt;/td&gt;
&lt;td&gt;Big-bang rewrite. Coexistence isn't worth it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Has clear routes and pages&lt;/td&gt;
&lt;td&gt;Strangler fig at route level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is one giant page, or has a shared shell everywhere&lt;/td&gt;
&lt;td&gt;Strangler fig at widget level, often with a micro-frontend host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Has a data layer, store or service every page uses&lt;/td&gt;
&lt;td&gt;Branch by abstraction around it, early&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Has few tests&lt;/td&gt;
&lt;td&gt;Write tests at the user level (end-to-end) first, then any strategy. Without them, nothing tells you a slice is done.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Has many teams shipping in parallel&lt;/td&gt;
&lt;td&gt;Vertical slices owned by one team each, behind flags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Must keep shipping features during the migration&lt;/td&gt;
&lt;td&gt;Anything except big-bang&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Measuring Progress and Knowing When You're Done
&lt;/h2&gt;

&lt;p&gt;Incremental migrations have a known failure mode: &lt;strong&gt;they stall at 80%.&lt;/strong&gt; The easy routes are done, the hard core remains, and the team gets pulled onto feature work. You end up running two frameworks forever. Measuring progress openly is the best defense:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Routes served by the new app&lt;/strong&gt;, as a percentage of traffic, not just a count. Ten rarely used admin pages don't equal one home feed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lines of legacy code left&lt;/strong&gt;, falling every week. If it stops falling, something is wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seams and flags still open.&lt;/strong&gt; Each one is temporary and should have an owner and an end date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parity results.&lt;/strong&gt; For each slice: do the same tests pass against old and new, and does the parallel run show zero differences?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And a short definition of done that every slice has to meet before its old code is deleted: same tests passing on both, flag at 100% for a full release cycle with no rollbacks, no remaining imports of the old code, and the old code removed in the same sprint.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Big-bang rewrites fail on a deadline. Incremental migrations fail on discipline. Pick the failure you can manage.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Bottom line:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Get new code in front of real users early.&lt;/strong&gt; Every strategy that works is built on this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strangle by what users see,&lt;/strong&gt; route by route, not file by file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put a seam around anything every page depends on,&lt;/strong&gt; and switch it with a flag, not a merge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move whole features, not layers.&lt;/strong&gt; A finished slice ships. A finished layer doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete as you go,&lt;/strong&gt; including the scaffolding. The migration ends when the seams and flags are gone, not when the last route moves.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Further in This Series
&lt;/h2&gt;

&lt;p&gt;Each of these takes the strategies above and applies them to one starting point or one design decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://cobuildx.ai/blog/angular-to-react-migration-guide" rel="noopener noreferrer"&gt;Moving from Angular to React: A Practical Guide&lt;/a&gt;, with the strangler fig worked end to end.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cobuildx.ai/blog/ember-to-react-migration-guide" rel="noopener noreferrer"&gt;Leaving Ember for React: What Your Conventions Were Hiding&lt;/a&gt;, where Ember Data is a classic branch-by-abstraction candidate.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cobuildx.ai/blog/backbone-to-react-migration-guide" rel="noopener noreferrer"&gt;The Framework Nobody Wrote Down: Taking a Backbone App to React&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cobuildx.ai/blog/jquery-to-react-migration-guide" rel="noopener noreferrer"&gt;Who Updates the Total? Taking a jQuery App to React&lt;/a&gt;, mostly widget-level strangling.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cobuildx.ai/blog/vanilla-js-to-react-migration-guide" rel="noopener noreferrer"&gt;The State Is in the DOM: Taking a Vanilla JavaScript App to React&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cobuildx.ai/blog/client-patterns-mvc-mvvm" rel="noopener noreferrer"&gt;Client Patterns: MVC, MVVM, and What Came After&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cobuildx.ai/blog/one-way-two-way-binding" rel="noopener noreferrer"&gt;One-Way vs Two-Way Binding&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cobuildx.ai/blog/csr-vs-ssr" rel="noopener noreferrer"&gt;Client-Side vs Server-Side Rendering&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Our Migration Plugins
&lt;/h2&gt;

&lt;p&gt;Choosing a strategy starts with knowing the app: every route, every shared service, every piece of hidden wiring. That map is slow to build by hand. We've packaged how we do it as free, open-source plugins for Claude Code, one per starting point. Each one plans an incremental, strangler-fig migration by default:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plugin&lt;/th&gt;
&lt;th&gt;Starts from&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/cobuild-tech/cbx-plugins/tree/main/ember-to-react" rel="noopener noreferrer"&gt;&lt;code&gt;ember-to-react&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Ember Classic and Octane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/cobuild-tech/cbx-plugins/tree/main/angular-to-react" rel="noopener noreferrer"&gt;&lt;code&gt;angular-to-react&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Angular 2+ and AngularJS 1.x, including hybrid ngUpgrade apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/cobuild-tech/cbx-plugins/tree/main/backbone-to-react" rel="noopener noreferrer"&gt;&lt;code&gt;backbone-to-react&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Backbone.js, Marionette, jQuery templates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/cobuild-tech/cbx-plugins/tree/main/jquery-to-react" rel="noopener noreferrer"&gt;&lt;code&gt;jquery-to-react&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;jQuery apps, plugins and jQuery UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/cobuild-tech/cbx-plugins/tree/main/vanillajs-to-react" rel="noopener noreferrer"&gt;&lt;code&gt;vanillajs-to-react&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Plain HTML, CSS and JavaScript&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each plugin comes with a migration skill, a &lt;code&gt;/&amp;lt;plugin&amp;gt;:plan&lt;/code&gt; command that writes a migration plan (strategy, units, route map, test plan and risks) without touching your code, a read-only inventory agent that maps the old app, a parity-reviewer agent that compares each migrated piece with its original, and a bundled Context7 server for up-to-date library docs. To install one in Claude 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="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt; &lt;span class="nx"&gt;marketplace&lt;/span&gt; &lt;span class="nx"&gt;add&lt;/span&gt; &lt;span class="nx"&gt;cobuild&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;tech&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;cbx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;plugins&lt;/span&gt;
&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt; &lt;span class="nx"&gt;install&lt;/span&gt; &lt;span class="nx"&gt;ember&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;react&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;cbx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;plugins&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They also work in Cursor, in VS Code with GitHub Copilot, and in the GitHub Copilot CLI. Progress is kept in a &lt;code&gt;.migration/&lt;/code&gt; folder in your repo, so a migration can carry on across sessions and teammates. The source and setup steps are at &lt;a href="https://github.com/cobuild-tech/cbx-plugins" rel="noopener noreferrer"&gt;cobuild-tech/cbx-plugins&lt;/a&gt;, and issues are welcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  About MigrateX
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxywolgtbstuveflre5yv.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxywolgtbstuveflre5yv.png" alt="MigrateX" width="799" height="293"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MigrateX is our platform and service for moving software, data and infrastructure from one technology to another. It combines automation with experienced engineers. The automation does the repetitive work quickly, and the engineers make the tricky decisions, like where the strangler boundary runs and which shared services need a seam first.&lt;/p&gt;

&lt;p&gt;A MigrateX project usually runs like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Assessment.&lt;/strong&gt; We map every route, shared service and hidden dependency in your app, so you see where the work is before anyone agrees on a timeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan.&lt;/strong&gt; With your team, we choose the strategy, the slice order, and what can simply be deleted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration, step by step.&lt;/strong&gt; Automation handles the repetitive parts. Our engineers handle the router, the seams and the code that connects old and new.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proof that it works.&lt;/strong&gt; The same tests run against both versions. A piece only counts as moved when it passes on both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handover.&lt;/strong&gt; We remove the temporary code, the seams and the flags, and leave your team with a codebase they understand and own.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your app keeps running and shipping features the whole time, so there's no big launch day. Our &lt;a href="https://github.com/cobuild-tech/migratex-examples" rel="noopener noreferrer"&gt;migratex-examples&lt;/a&gt; repo has small apps in each legacy framework next to their migrated versions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Planning a migration and not sure which strategy fits? Tell us a little about your app through &lt;a href="https://cobuildx.ai/contact" rel="noopener noreferrer"&gt;Contact Us&lt;/a&gt;, and we'll start with a free chat about where the work is likely to be.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Further reading&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Martin Fowler. &lt;em&gt;Strangler Fig Application&lt;/em&gt;. &lt;a href="https://martinfowler.com/bliki/StranglerFigApplication.html" rel="noopener noreferrer"&gt;https://martinfowler.com/bliki/StranglerFigApplication.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Martin Fowler. &lt;em&gt;Branch By Abstraction&lt;/em&gt;. &lt;a href="https://martinfowler.com/bliki/BranchByAbstraction.html" rel="noopener noreferrer"&gt;https://martinfowler.com/bliki/BranchByAbstraction.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Paul Hammant. &lt;em&gt;Branch by Abstraction&lt;/em&gt;. &lt;a href="https://www.branchbyabstraction.com/" rel="noopener noreferrer"&gt;https://www.branchbyabstraction.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Trunk Based Development. &lt;em&gt;Branch by Abstraction&lt;/em&gt;. &lt;a href="https://trunkbaseddevelopment.com/branch-by-abstraction/" rel="noopener noreferrer"&gt;https://trunkbaseddevelopment.com/branch-by-abstraction/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Danilo Sato. &lt;em&gt;Parallel Change&lt;/em&gt;. &lt;a href="https://martinfowler.com/bliki/ParallelChange.html" rel="noopener noreferrer"&gt;https://martinfowler.com/bliki/ParallelChange.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Ian Cartwright, Rob Horn and James Lewis. &lt;em&gt;Patterns of Legacy Displacement&lt;/em&gt;. &lt;a href="https://martinfowler.com/articles/patterns-legacy-displacement/" rel="noopener noreferrer"&gt;https://martinfowler.com/articles/patterns-legacy-displacement/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pete Hodgson. &lt;em&gt;Feature Toggles (aka Feature Flags)&lt;/em&gt;. &lt;a href="https://martinfowler.com/articles/feature-toggles.html" rel="noopener noreferrer"&gt;https://martinfowler.com/articles/feature-toggles.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cam Jackson. &lt;em&gt;Micro Frontends&lt;/em&gt;. &lt;a href="https://martinfowler.com/articles/micro-frontends.html" rel="noopener noreferrer"&gt;https://martinfowler.com/articles/micro-frontends.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Microsoft. &lt;em&gt;Strangler Fig pattern&lt;/em&gt;. &lt;a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/strangler-fig" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/azure/architecture/patterns/strangler-fig&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Microsoft. &lt;em&gt;Anti-corruption Layer pattern&lt;/em&gt;. &lt;a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub. &lt;em&gt;Scientist&lt;/em&gt;: a library for carefully refactoring critical paths. &lt;a href="https://github.com/github/scientist" rel="noopener noreferrer"&gt;https://github.com/github/scientist&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Joel Spolsky. &lt;em&gt;Things You Should Never Do, Part I&lt;/em&gt;. &lt;a href="https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/" rel="noopener noreferrer"&gt;https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sam Newman. &lt;em&gt;Monolith to Microservices&lt;/em&gt;. O'Reilly, 2019. &lt;a href="https://samnewman.io/books/monolith-to-microservices/" rel="noopener noreferrer"&gt;https://samnewman.io/books/monolith-to-microservices/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Source code&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CobuildX AI. &lt;em&gt;cbx-plugins&lt;/em&gt;: Claude Code plugins for Backbone, Angular, Ember, jQuery and Vanilla JS to React migrations. &lt;a href="https://github.com/cobuild-tech/cbx-plugins" rel="noopener noreferrer"&gt;https://github.com/cobuild-tech/cbx-plugins&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CobuildX AI. &lt;em&gt;migratex-examples&lt;/em&gt;: small jQuery, Backbone, Angular, Ember and Vanilla JS apps and their migrated versions. &lt;a href="https://github.com/cobuild-tech/migratex-examples" rel="noopener noreferrer"&gt;https://github.com/cobuild-tech/migratex-examples&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cobuildx.ai/blog/frontend-migration-strategies" rel="noopener noreferrer"&gt;CobuildX blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>frontend</category>
      <category>architecture</category>
      <category>react</category>
    </item>
    <item>
      <title>Jev as a Red-Teaming Guardrail: Putting TypeSafe's Model to Work in AI Security</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Thu, 08 Oct 2026 11:03:01 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/jev-as-a-red-teaming-guardrail-putting-typesafes-model-to-work-in-ai-security-1eno</link>
      <guid>https://dev.to/cobuildx-ai/jev-as-a-red-teaming-guardrail-putting-typesafes-model-to-work-in-ai-security-1eno</guid>
      <description>&lt;p&gt;&lt;em&gt;We ran TypeSafe's Jev as a prompt guardrail on 1,580 red-teaming prompts against GPT-5-mini, Claude Haiku 4.5 and three open-source guard models. It was five to six times faster, 6–27× cheaper, and had the highest block accuracy of the hosted models.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;TypeSafe's Jev promises LLM-grade decisions at a fraction of the time and cost. We put that claim to a practical test: using Jev as a guardrail that screens prompts before they reach an AI agent. Across 1,580 red-teaming prompts from public jailbreak, prompt-injection and harmful-content benchmarks, we compared Jev with OpenAI GPT-5-mini, Claude Haiku 4.5 and three open-source guard models on speed, cost, and how well each one decides what to block and names the risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hype around Jev
&lt;/h2&gt;

&lt;p&gt;TypeSafe launched Jev with this headline [1]:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy9o8q6flpsvpfg8y9t33.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy9o8q6flpsvpfg8y9t33.png" alt="TypeSafe's launch claim: 193.6x faster, 444.6x cheaper" width="800" height="237"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why the hype? Orders of magnitude? Is comparing it to an LLM even fair? Let's dig into where that speed comes from, and whether it holds up.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Jev differs from an LLM
&lt;/h2&gt;

&lt;p&gt;At a high level, Jev predicts the probability of each option in a set of choices or actions that you define, using the prompt or context you give it as the state. You might ask "should this input be blocked?" and "which of these 24 risk categories does it fall into?". Jev returns calibrated probabilities for every option in a single pass.&lt;/p&gt;

&lt;p&gt;An LLM works differently. It generates its output one token at a time (The 'decode' phase), and each step depends on the tokens it has already produced. This stepwise &lt;em&gt;decode&lt;/em&gt; phase is where most of an LLM's latency comes from. To use an LLM as a classifier, you ask it to write out its decision, usually as JSON, and then parse that text.&lt;/p&gt;

&lt;p&gt;That difference has practical consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The output is typed.&lt;/strong&gt; Jev returns probabilities and choices, not free text. There is nothing to parse, and no output can arrive malformed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cost structure is different.&lt;/strong&gt; Jev bills for input tokens only; its output is free [2]. An LLM bills for both, and at a higher rate for output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency does not grow with the answer.&lt;/strong&gt; A single scoring pass does not slow down as the answer gets longer, because there is no answer to write out token by token.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where Jev is a better fit
&lt;/h2&gt;

&lt;p&gt;Jev is not a replacement for an LLM. It does not write answers, summarise documents or generate code. What it targets is the large class of tasks inside AI systems where the output is a &lt;em&gt;decision&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;guardrails that decide whether an input or action should be blocked&lt;/li&gt;
&lt;li&gt;routing requests to the right model, tool or team&lt;/li&gt;
&lt;li&gt;classification, moderation and triage at high volume&lt;/li&gt;
&lt;li&gt;choosing the next action in an agent workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These decisions often sit in the critical path of every request. In that position, each additional second and each additional cent is paid on every call. That is where a model built only for decisions has the most to gain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we tested: Jev as a red-teaming guardrail
&lt;/h2&gt;

&lt;p&gt;Agent security is a natural test for this kind of model. A guardrail has to read one input and make two calls:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Should this input be blocked?&lt;/strong&gt; It might be a harmful request, a jailbreak attempt, or a prompt injection hidden inside a document or tool response.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What kind of risk is it?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the second question, every model chooses from &lt;strong&gt;24 risk categories&lt;/strong&gt;, and the risk category each prompt carries in its source dataset is the ground truth. 22 of these come from RedBench's risk taxonomy [3]. We added &lt;em&gt;No risk&lt;/em&gt; for benign inputs and &lt;em&gt;Prompt attack&lt;/em&gt; for jailbreak and injection attempts that target no specific harm.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Risk category&lt;/th&gt;
&lt;th&gt;Covers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No risk&lt;/td&gt;
&lt;td&gt;Benign request; safe to answer normally, even if it uses alarming words&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Abusive content&lt;/td&gt;
&lt;td&gt;Harassment, insults, bullying or degrading language&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bias content&lt;/td&gt;
&lt;td&gt;Stereotyping or unfair treatment of groups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CBRN information or capabilities&lt;/td&gt;
&lt;td&gt;Chemical, biological, radiological or nuclear weapons knowledge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Child safety&lt;/td&gt;
&lt;td&gt;Content endangering or sexualising minors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confabulation&lt;/td&gt;
&lt;td&gt;Eliciting fabricated facts presented as true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crime content&lt;/td&gt;
&lt;td&gt;Facilitating theft, fraud, drugs or other crimes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cybersecurity threats (beyond malware)&lt;/td&gt;
&lt;td&gt;Hacking, intrusion, phishing, system disruption&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data privacy&lt;/td&gt;
&lt;td&gt;Obtaining or exposing personal or confidential data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decision-making in critical systems&lt;/td&gt;
&lt;td&gt;Unsafe automated decisions in medical, legal, financial or safety-critical settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Economic harm&lt;/td&gt;
&lt;td&gt;Market manipulation, scams, financial damage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Election interference&lt;/td&gt;
&lt;td&gt;Voter manipulation or election disinformation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Environmental harm&lt;/td&gt;
&lt;td&gt;Pollution, ecological damage, wildlife harm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extremism and radicalization&lt;/td&gt;
&lt;td&gt;Promoting extremist ideology or recruitment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hateful content&lt;/td&gt;
&lt;td&gt;Hate speech targeting protected groups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Illegal weapons (non-CBRN)&lt;/td&gt;
&lt;td&gt;Building or acquiring illegal conventional weapons or explosives&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intellectual property&lt;/td&gt;
&lt;td&gt;Piracy or copyright and trademark infringement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Malware code&lt;/td&gt;
&lt;td&gt;Writing or deploying malicious software&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Misinformation&lt;/td&gt;
&lt;td&gt;False or misleading claims presented as fact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-harm content&lt;/td&gt;
&lt;td&gt;Suicide, self-injury or eating-disorder encouragement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sexual content&lt;/td&gt;
&lt;td&gt;Explicit or non-consensual sexual content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Violent content&lt;/td&gt;
&lt;td&gt;Violence, threats or physical harm to others&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Other&lt;/td&gt;
&lt;td&gt;Harmful in a way not covered by the categories above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt attack&lt;/td&gt;
&lt;td&gt;Jailbreak or prompt-injection attempt when no more specific category applies&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Data
&lt;/h3&gt;

&lt;p&gt;Our starting point was RedBench [3], which consolidates 37 red-teaming benchmarks. We added dedicated jailbreak and prompt-injection datasets [4–11], including agent transcripts where the attack is planted in a tool response [9]. From a pool of 41,443 prompts, we evaluated a stratified sample of &lt;strong&gt;1,580&lt;/strong&gt; (1,310 harmful, 270 benign), drawn from 14 datasets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Models
&lt;/h3&gt;

&lt;p&gt;We gave the same task, with identical instructions and the same 24-category risk taxonomy, to three hosted models: &lt;strong&gt;TypeSafe Jev&lt;/strong&gt; [2], &lt;strong&gt;OpenAI GPT-5-mini&lt;/strong&gt; [12] and &lt;strong&gt;Anthropic Claude Haiku 4.5&lt;/strong&gt; [13].&lt;/p&gt;

&lt;h2&gt;
  
  
  Results and observations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Speed, cost and quality
&lt;/h3&gt;

&lt;p&gt;Jev's median latency was &lt;strong&gt;0.17 seconds&lt;/strong&gt;, against 1.00 s for GPT-5-mini and 0.89 s for Claude Haiku 4.5. That is about &lt;strong&gt;six times faster&lt;/strong&gt; than GPT-5-mini and more than five times faster than Haiku. At the 95th percentile, Jev stayed at 0.26 s, while both LLMs were above 1.3 s.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs2kczup86ak6wutgnsgh.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs2kczup86ak6wutgnsgh.png" alt="Latency per prompt" width="799" height="254"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Classifying all 1,580 prompts cost &lt;strong&gt;$0.08&lt;/strong&gt; with Jev, &lt;strong&gt;$0.48&lt;/strong&gt; with GPT-5-mini and &lt;strong&gt;$2.23&lt;/strong&gt; with Claude Haiku 4.5 at list prices. That makes Jev about &lt;strong&gt;6× cheaper than GPT-5-mini&lt;/strong&gt; and &lt;strong&gt;27× cheaper than Haiku&lt;/strong&gt; on the same workload.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgzct81ragud7izkuws8u.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgzct81ragud7izkuws8u.png" alt="Cost per prompt" width="800" height="353"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our measured gap is smaller than the headline figures TypeSafe published. That is expected. Their numbers describe workflows of System One tasks against their chosen baselines. Ours come from a single adversarial classification task, compared against two widely used, low-cost LLMs. The direction matches their claim; the size of the gap depends on the task and on what you compare against.&lt;/p&gt;

&lt;p&gt;Speed and cost only matter if the decisions hold up. Across all prompts, Jev flagged harm correctly &lt;strong&gt;85.5%&lt;/strong&gt; of the time (block accuracy), against 84.1% for GPT-5-mini and 80.1% for Claude Haiku 4.5.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fchr3h4an8cp8tob38xzw.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fchr3h4an8cp8tob38xzw.png" alt="Overall block accuracy" width="800" height="254"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On naming the exact risk category out of 24 (categorization accuracy), Jev scored &lt;strong&gt;58.0%&lt;/strong&gt; against 56.0% and 55.4%.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Felva61ce4tgna3nrvvmq.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Felva61ce4tgna3nrvvmq.png" alt="Overall categorization accuracy" width="800" height="254"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Its macro-F1 across the 24 categories, which weights every category equally regardless of size, was &lt;strong&gt;46.5%&lt;/strong&gt; against 41.7% and 38.1%.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz7xhnh6vqoynduplsijw.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz7xhnh6vqoynduplsijw.png" alt="Overall macro-F1" width="800" height="254"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Detection quality
&lt;/h3&gt;

&lt;p&gt;Categorization accuracy for each risk category with more than 20 labelled prompts:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8gn8xl55vmshqdykm18u.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8gn8xl55vmshqdykm18u.png" alt="Categorization accuracy by risk category, categories with more than 20 labelled prompts" width="799" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Of these categories, Jev has the highest categorization accuracy in nine: crime, abusive content, misinformation, cybersecurity threats, bias, data privacy, sexual content, economic harm and decision-making in critical systems. GPT-5-mini leads on hateful content, malware, violent content and illegal weapons. Haiku is strongest at recognising benign inputs (&lt;em&gt;No risk&lt;/em&gt;) and prompt attacks, and ties with GPT-5-mini ahead of Jev on self-harm.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comparison with task-specific models
&lt;/h3&gt;

&lt;p&gt;We also ran three open-source models built specifically for guardrailing, on a T4 GPU: &lt;strong&gt;Qwen3Guard-Gen-4B&lt;/strong&gt; [14], &lt;strong&gt;Llama Prompt Guard 2 86M&lt;/strong&gt; [15] and &lt;strong&gt;ProtectAI DeBERTa-v3 prompt-injection v2&lt;/strong&gt; [16]. None of them answers in the 24 risk categories, so they are compared with Jev on block accuracy only.&lt;/p&gt;

&lt;p&gt;To compare performance across attack domains, we grouped the source benchmarks into nine &lt;strong&gt;benchmark groups&lt;/strong&gt; based on the kind of prompts each contains. The groups are used only to aggregate results; they are not the labels that the models predict.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Benchmark group&lt;/th&gt;
&lt;th&gt;What it tests&lt;/th&gt;
&lt;th&gt;Benchmarks included&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Harmful instruction and question sets&lt;/td&gt;
&lt;td&gt;Plainly worded harmful requests and questions&lt;/td&gt;
&lt;td&gt;AdvBench, CatQA, CoNA, ControversialInstructions, DAN, DoNotAnswer, ForbiddenQuestions, GPTFuzzer, HarmBench, HarmfulQ, HarmfulQA, JBB-Behaviors, MaliciousInstruct, MaliciousInstructions, MedSafetyBench, QHarm, SGBench, StrongREJECT (all via RedBench [3])&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cyberattack assistance&lt;/td&gt;
&lt;td&gt;Requests for help with hacking, intrusion and malware&lt;/td&gt;
&lt;td&gt;CyberattackAssistance (via RedBench [3])&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mixed safety suites&lt;/td&gt;
&lt;td&gt;Broad safety suites whose prompts span many harm types&lt;/td&gt;
&lt;td&gt;XSafety, JADE (via RedBench [3])&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jailbreak prompt collections&lt;/td&gt;
&lt;td&gt;DAN-style, role-play and "developer mode" templates, with ordinary prompts as benign contrasts&lt;/td&gt;
&lt;td&gt;In-the-wild jailbreak and regular prompts [4], JailbreakBench JBC [5], Salad-Data jailbreak templates [6], jailbreak-classification and ChatGPT-Jailbreak-Prompts [10], LatentJailbreak (via RedBench [3])&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automated jailbreak attacks&lt;/td&gt;
&lt;td&gt;Attacks generated automatically (GCG, PAIR, random search, AutoDAN, GPTFuzzer, TAP)&lt;/td&gt;
&lt;td&gt;JailbreakBench GCG, PAIR and random-search artifacts [5], Salad-Data AutoDAN, GCG, GPTFuzzer and TAP attacks [6]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct prompt injection sets&lt;/td&gt;
&lt;td&gt;"Ignore previous instructions" and secret-extraction attempts, with benign contrasts&lt;/td&gt;
&lt;td&gt;Gandalf ignore-instructions and summarization (via RedBench [3]), Lakera Gandalf [8], deepset prompt-injections [7], safe-guard-prompt-injection [10]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Indirect prompt injection (InjecAgent)&lt;/td&gt;
&lt;td&gt;Instructions planted in tool responses to hijack an agent&lt;/td&gt;
&lt;td&gt;InjecAgent data-stealing and direct-harm cases [9], plus clean counterparts built for this study&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Toxic statements and unsafe advice&lt;/td&gt;
&lt;td&gt;Toxic statements and prompts that invite unsafe advice&lt;/td&gt;
&lt;td&gt;ToxiGen, SafeText, PhysicalSafetyInstructions (via RedBench [3])&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Over-refusal suites&lt;/td&gt;
&lt;td&gt;Benign prompts that &lt;em&gt;sound&lt;/em&gt; dangerous, plus unsafe contrasts&lt;/td&gt;
&lt;td&gt;XSTest and CoCoNot [11], OR-Bench &lt;a href="https://dev.tovia%20RedBench%20[3]"&gt;11&lt;/a&gt;, JBB-Behaviors benign set [5]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Qwen3Guard is scored on every benchmark group. Prompt Guard 2 and ProtectAI detect prompt attacks only, so they appear only in the jailbreak and prompt-injection groups they were run on in full.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F19uy4plh7twmcxthbi38.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F19uy4plh7twmcxthbi38.png" alt="Block accuracy by benchmark group, Jev and task-specific models" width="800" height="398"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Jev matches or beats every task-specific model in eight of the nine benchmark groups.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Clear leads:&lt;/strong&gt; direct prompt injection (95%, against 88% for ProtectAI and 80% for Qwen3Guard), cyberattack assistance (83% against 67%), toxic statements and unsafe advice (49% against 38%) and indirect prompt injection (77% against 70%).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Narrow leads:&lt;/strong&gt; harmful instructions (92% against 90%) and mixed safety suites (47% against 42%).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level:&lt;/strong&gt; automated jailbreaks (96% for both) and over-refusal (72% for both).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behind:&lt;/strong&gt; jailbreak prompt collections, where Qwen3Guard is slightly ahead (93.3% against 92.9%).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two prompt-attack classifiers trail Jev throughout. Prompt Guard 2 is behind Jev and Qwen3Guard in every group it covers. ProtectAI beats Qwen3Guard on direct injection but not Jev. Both score 33% on indirect injection hidden in tool responses.&lt;/p&gt;

&lt;p&gt;Two caveats apply:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mixed safety suites and toxic statements are hard for every model, with all scores below 50%.&lt;/li&gt;
&lt;li&gt;Indirect prompt injection remains Jev's weakest group against the hosted LLMs, which both reached 91% there.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this means for enterprises
&lt;/h2&gt;

&lt;p&gt;For most organisations, the expensive part of AI safety is not a single model call. It is making that call on every request, every agent step, and every test case in every evaluation run. A guardrail that sits in front of a production agent may run millions of times a month. An evaluation suite that re-scores thousands of cases after each prompt or model change runs constantly during development.&lt;/p&gt;

&lt;p&gt;In those settings, the gap between a one-second LLM judgement and a sub-200-millisecond typed decision is not a rounding error. It decides whether security checks can run inline or have to be sampled, and whether evaluation runs on every change or once a week. Customers running their evaluation workloads through OptiX with Jev are already seeing reduced evaluation costs and lower latency, with performance on par with or ahead of the LLMs.&lt;/p&gt;

&lt;p&gt;The broader lesson from this study is that decision tasks and generation tasks do not need the same model. Matching the model to the job, and measuring it on your own data, pays off quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Explore OptiX
&lt;/h2&gt;

&lt;p&gt;OptiX is our platform for testing, evaluating and monitoring AI systems. It brings together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Security scans&lt;/strong&gt; for jailbreaks, prompt injection and other adversarial inputs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-agent evaluation&lt;/strong&gt; across complex, multi-step workflows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt; for AI systems in production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use-case evaluation&lt;/strong&gt; for RAG, text-to-SQL, document parsing, entity extraction, summarisation and more&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jev support&lt;/strong&gt;, so fast, low-cost typed decisions can power guardrails and evaluations alongside LLM-based judges&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;To see how your own agents hold up, visit &lt;strong&gt;&lt;a href="https://optix.cobuildx.ai/" rel="noopener noreferrer"&gt;optix.cobuildx.ai&lt;/a&gt;&lt;/strong&gt; and get in touch with our team.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Benchmark and claim&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TypeSafe AI. "193.6x faster, 444.6x cheaper" launch claim. &lt;a href="https://typesafe.ai/" rel="noopener noreferrer"&gt;https://typesafe.ai/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;TypeSafe AI. Jev model documentation. &lt;a href="https://docs.typesafe.ai/models" rel="noopener noreferrer"&gt;https://docs.typesafe.ai/models&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Q.-A. Dang, C. Ngo, T.-S. Hy. &lt;em&gt;RedBench: A Universal Dataset for Comprehensive Red Teaming of Large Language Models.&lt;/em&gt; arXiv:2601.03699, 2026. Dataset: &lt;a href="https://huggingface.co/datasets/knoveleng/redbench" rel="noopener noreferrer"&gt;https://huggingface.co/datasets/knoveleng/redbench&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Jailbreak and prompt-injection datasets&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;X. Shen et al. &lt;em&gt;"Do Anything Now": Characterizing and Evaluating In-The-Wild Jailbreak Prompts on Large Language Models.&lt;/em&gt; ACM CCS 2024. arXiv:2308.03825. Dataset: &lt;a href="https://huggingface.co/datasets/TrustAIRLab/in-the-wild-jailbreak-prompts" rel="noopener noreferrer"&gt;https://huggingface.co/datasets/TrustAIRLab/in-the-wild-jailbreak-prompts&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;P. Chao et al. &lt;em&gt;JailbreakBench: An Open Robustness Benchmark for Jailbreaking Large Language Models.&lt;/em&gt; arXiv:2404.01318, 2024. Artifacts: &lt;a href="https://github.com/JailbreakBench/artifacts" rel="noopener noreferrer"&gt;https://github.com/JailbreakBench/artifacts&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;L. Li et al. &lt;em&gt;SALAD-Bench: A Hierarchical and Comprehensive Safety Benchmark for Large Language Models.&lt;/em&gt; arXiv:2402.05044, 2024. Dataset: &lt;a href="https://huggingface.co/datasets/OpenSafetyLab/Salad-Data" rel="noopener noreferrer"&gt;https://huggingface.co/datasets/OpenSafetyLab/Salad-Data&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;deepset. prompt-injections dataset. &lt;a href="https://huggingface.co/datasets/deepset/prompt-injections" rel="noopener noreferrer"&gt;https://huggingface.co/datasets/deepset/prompt-injections&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Lakera. gandalf_ignore_instructions dataset. &lt;a href="https://huggingface.co/datasets/Lakera/gandalf_ignore_instructions" rel="noopener noreferrer"&gt;https://huggingface.co/datasets/Lakera/gandalf_ignore_instructions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Q. Zhan et al. &lt;em&gt;InjecAgent: Benchmarking Indirect Prompt Injections in Tool-Integrated Large Language Model Agents.&lt;/em&gt; arXiv:2403.02691, 2024. &lt;a href="https://github.com/uiuc-kang-lab/InjecAgent" rel="noopener noreferrer"&gt;https://github.com/uiuc-kang-lab/InjecAgent&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Community datasets on Hugging Face: jackhhao/jailbreak-classification, xTRam1/safe-guard-prompt-injection, rubend18/ChatGPT-Jailbreak-Prompts.&lt;/li&gt;
&lt;li&gt;Over-refusal benchmarks: P. Röttger et al. &lt;em&gt;XSTest: A Test Suite for Identifying Exaggerated Safety Behaviours in Large Language Models.&lt;/em&gt; arXiv:2308.01263, 2023; F. Brahman et al. &lt;em&gt;The Art of Saying No: Contextual Noncompliance in Language Models&lt;/em&gt; (CoCoNot). arXiv:2407.12043, 2024; J. Cui et al. &lt;em&gt;OR-Bench: An Over-Refusal Benchmark for Large Language Models.&lt;/em&gt; arXiv:2405.20947, 2024.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Models&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI. GPT-5-mini. &lt;a href="https://platform.openai.com/docs/models" rel="noopener noreferrer"&gt;https://platform.openai.com/docs/models&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Anthropic. Claude Haiku 4.5. &lt;a href="https://docs.anthropic.com/en/docs/about-claude/models" rel="noopener noreferrer"&gt;https://docs.anthropic.com/en/docs/about-claude/models&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;H. Zhao et al. &lt;em&gt;Qwen3Guard Technical Report.&lt;/em&gt; arXiv:2510.14276, 2025.&lt;/li&gt;
&lt;li&gt;S. Chennabasappa et al. &lt;em&gt;LlamaFirewall: An Open Source Guardrail System for Building Secure AI Agents&lt;/em&gt; (Llama Prompt Guard 2). arXiv:2505.03574, 2025.&lt;/li&gt;
&lt;li&gt;ProtectAI. deberta-v3-base-prompt-injection-v2. &lt;a href="https://huggingface.co/protectai/deberta-v3-base-prompt-injection-v2" rel="noopener noreferrer"&gt;https://huggingface.co/protectai/deberta-v3-base-prompt-injection-v2&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cobuildx.ai/blog/jev-red-teaming-guardrail" rel="noopener noreferrer"&gt;CobuildX blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>llm</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Collections AI: The Compliance Constraint That Shapes Everything</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Thu, 08 Oct 2026 11:02:25 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/collections-ai-the-compliance-constraint-that-shapes-everything-4edo</link>
      <guid>https://dev.to/cobuildx-ai/collections-ai-the-compliance-constraint-that-shapes-everything-4edo</guid>
      <description>&lt;p&gt;&lt;em&gt;AI can meaningfully improve collections performance within the regulatory framework — propensity scoring, channel optimisation, timing models. What it cannot do is automate the things TCPA, FDCPA, and state-level consumer protection laws prohibit. Here is what the boundary looks like.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Collections is one of the highest-value AI applications in financial services by the numbers — small improvements in recovery rates on large portfolios compound quickly. It is also one of the most heavily regulated operational functions at any lender. TCPA, FDCPA, state-level consumer protection laws, and the CFPB's interpretive guidance on unfair, deceptive, or abusive acts and practices all constrain what an AI-driven collections operation can do. The practical question is not whether to apply AI in collections — the ROI case is clear — but what AI can do within the constraint set.&lt;/p&gt;

&lt;p&gt;Collections AI projects fail in two ways. The first: the team builds a sophisticated propensity or segmentation model but does not integrate it into collections workflows in a way that changes collector behaviour. The model exists; nothing changes. The second: the team deploys automated outreach capabilities without adequately scoping the regulatory framework, and the compliance review catches problems that require significant redesign.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; TCPA and FDCPA do not prevent AI from improving collections — they define the boundaries within which AI can operate. Designing to those boundaries from the start produces a system that is both effective and defensible.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"We built the model. Then compliance reviewed the outreach automation we had planned and we had to redesign half of it."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What TCPA Actually Constrains
&lt;/h2&gt;

&lt;p&gt;The Telephone Consumer Protection Act (TCPA) restricts the use of automatic telephone dialling systems (ATDS) and artificial or prerecorded voice messages to contact consumers without their prior express written consent. In a collections context, this constrains automated outreach — AI-driven diallers, automated SMS campaigns, and prerecorded voice messages — significantly.&lt;/p&gt;

&lt;p&gt;Prior express written consent is required for autodialled calls or texts to mobile phones. For debt collection, this consent must be obtained at the time the credit relationship is established or through a subsequent consent process. Consent obtained at origination covers most account holders for initial collection contact, but consumers can revoke consent at any time, and revocations must be honoured immediately — a process that needs to be automated and audited.&lt;/p&gt;

&lt;p&gt;The TCPA does not prohibit human-initiated calls using manual dialling. Collections operations can use predictive diallers in a compliant way if the dialler does not meet the ATDS definition — an increasingly nuanced technical and legal question after the Facebook v. Duguid Supreme Court decision in 2021. Any collections AI involving automated outreach should be reviewed by counsel with TCPA expertise before deployment.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TCPA constrains automated outreach to mobile phones without consent — the consent management process is as important to design as the outreach automation itself&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  FDCPA: What You Can Say and When
&lt;/h2&gt;

&lt;p&gt;The Fair Debt Collection Practices Act (FDCPA) governs third-party debt collectors and imposes specific requirements on the timing, content, and frequency of collection communications. The CFPB's Regulation F, which took effect in 2021, extended some FDCPA principles and added specific guidance on electronic communications including email and text.&lt;/p&gt;

&lt;p&gt;Key constraints with direct implications for AI-driven collections:&lt;/p&gt;

&lt;p&gt;Contact frequency limits: Regulation F creates a presumption that contacting a consumer more than seven times in a seven-day period, or within seven days of a prior telephone conversation, violates the FDCPA. AI-driven outreach optimisation needs to track and enforce these limits across all communication channels.&lt;/p&gt;

&lt;p&gt;Time-of-day restrictions: calls are permitted only between 8am and 9pm in the consumer's local time zone. An AI scheduling model optimising for contact rates needs to incorporate this constraint — and needs to use the consumer's time zone, not the operations centre's.&lt;/p&gt;

&lt;p&gt;Cease and desist: if a consumer requests that contact cease, all communication must stop except to confirm receipt of the cease notice or to advise of specific legal action. This revocation process needs to be immediate and auditable.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Seven-in-seven contact frequency limits, time-of-day restrictions, and immediate cease-and-desist compliance are non-negotiable constraints that AI outreach must enforce&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What AI Can Do Within These Constraints
&lt;/h2&gt;

&lt;p&gt;Within the regulatory framework, AI delivers meaningful collections improvement through three specific applications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Propensity to pay modelling.&lt;/strong&gt; A model that predicts which accounts are most likely to pay in response to outreach — and under what conditions (settlement offer, payment plan, specific timing) — allows collections operations to prioritise contacts more effectively than simple days-past-due segmentation. Accounts with high propensity to pay at full balance get early personal contact; accounts that respond to settlement offers get offers earlier in the delinquency cycle when recovery is higher.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Channel preference optimisation.&lt;/strong&gt; Different consumers respond to different channels. Some respond to text, some to email, some to phone calls. A model trained on historical contact data can predict which channel is most likely to produce a response for a given consumer profile, reducing contact attempts while improving contact rates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timing optimisation.&lt;/strong&gt; Within the time-of-day constraints and the contact frequency limits, there is still meaningful variation in when a consumer is most likely to answer or respond. ML models trained on response rates by day of week and time of day — by consumer segment — can improve contact rates without increasing total contact attempts.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Propensity to pay modelling, channel preference, and timing optimisation deliver collections lift within the regulatory framework — together, improvements of 10–20% in recovery rates on targeted portfolios are realistic&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  State-Level Overlay
&lt;/h2&gt;

&lt;p&gt;Federal TCPA and FDCPA requirements are the floor. A significant number of states have consumer protection laws that are more restrictive, and compliance at the federal level does not mean compliance in every state where borrowers are located.&lt;/p&gt;

&lt;p&gt;California's Rosenthal Fair Debt Collection Practices Act applies FDCPA-equivalent requirements to first-party collectors (lenders collecting their own debt) in addition to third-party collectors — a broader scope than federal law. New York and other states have similar first-party coverage extensions.&lt;/p&gt;

&lt;p&gt;Several states have more restrictive contact frequency rules than Regulation F, shorter time windows for permissible calling hours, or additional disclosure requirements for automated communications.&lt;/p&gt;

&lt;p&gt;For a lender with a national portfolio, the compliance standard is the most restrictive applicable state law for each consumer, not the most permissive interpretation of federal law. A collections AI system with national scope needs to implement state-level rule variations or operate to the most conservative standard across the full portfolio.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;State-level consumer protection laws can be more restrictive than federal TCPA and FDCPA — compliance requires the most restrictive applicable standard for each consumer's state&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Building the Business Case Within the Constraints
&lt;/h2&gt;

&lt;p&gt;The ROI case for collections AI is real within these constraints. The constraint set limits automated outreach, but it does not limit propensity scoring, segmentation, channel optimisation, or timing models — all of which improve outcomes without requiring automation of contact initiation.&lt;/p&gt;

&lt;p&gt;Realistic targets for a well-scoped collections AI program: 10–20% improvement in early-stage recovery rates through better prioritisation and channel selection on accounts where propensity to pay is high. 15–25% reduction in average collection cost per recovered dollar through more efficient contact allocation. Measurable reduction in complaints from improved compliance with contact frequency and timing rules.&lt;/p&gt;

&lt;p&gt;The compliance investment — consent management, revocation tracking, state-level rule implementation, audit logging — is not small. It is also not optional, and treating it as overhead rather than part of the system design leads to the pattern of building first and redesigning after compliance review. The right approach: scope the compliance infrastructure at the start, design the AI system to operate within it, and build the business case on what is achievable within that framework.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;10–20% early-stage recovery improvement and 15–25% cost reduction per recovered dollar are achievable within the regulatory framework — scope the compliance infrastructure at the start, not after&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cobuildx.ai/blog/collections-ai-compliance-constraints" rel="noopener noreferrer"&gt;CobuildX blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>fintech</category>
      <category>machinelearning</category>
      <category>compliance</category>
    </item>
    <item>
      <title>Credit Risk Scoring: The Data Problems That Haven't Changed</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Thu, 08 Oct 2026 11:01:49 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/credit-risk-scoring-the-data-problems-that-havent-changed-3n8c</link>
      <guid>https://dev.to/cobuildx-ai/credit-risk-scoring-the-data-problems-that-havent-changed-3n8c</guid>
      <description>&lt;p&gt;&lt;em&gt;Better models have not solved the thin-file problem, the data quality issues in alternative data, or the validation requirements that govern what can be deployed in regulated lending. The data problems are still the hard part.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Credit risk modelling has changed significantly over the past decade. Gradient boosting has replaced logistic regression as the standard approach at most sophisticated lenders. Alternative data — cash flow data, rental payment history, utility payments — has broadened the credit-visible population. Explainability tooling has made complex models more deployable in regulated environments. What has not changed: the data quality problems that determine whether any model, regardless of sophistication, produces reliable predictions.&lt;/p&gt;

&lt;p&gt;The limiting factor in credit risk modelling is not model architecture. Given reasonable data, a well-tuned gradient boosting model and a well-tuned logistic regression produce different but both useful predictions. The limiting factors are: the thin-file problem (a significant fraction of credit-worthy borrowers have insufficient traditional credit history to model reliably), the alternative data quality problem (much of the data that could address thin-file has its own reliability and coverage gaps), and the model validation requirements that govern what can be deployed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; Lenders who have improved credit risk modelling in recent years have done so primarily through better data — specifically cash flow data and expanded credit bureau data — not better model architectures. The model is less important than the signal.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"We spent six months building a more sophisticated model. When we went back and cleaned the training data, the simple model performed just as well."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Thin-File Problem
&lt;/h2&gt;

&lt;p&gt;Approximately 45 million Americans have thin or no credit files — too little traditional credit history for the major bureaus to generate a reliable score. This population is not uniformly high risk. It includes recent immigrants with strong financial management histories in their home countries, young adults who have avoided debt but have stable income, and individuals who have historically used cash and community lending. A lender that cannot score them declines credit-worthy borrowers and cedes that business to competitors who can.&lt;/p&gt;

&lt;p&gt;Traditional bureau data does not solve the thin-file problem because the thin-file is definitionally not in the bureau data. Expanding the modelling approach requires either incorporating non-traditional data sources (rental payments, utility payments, cash flow data) or accepting that a segment of the credit-worthy population will not be scoreable through traditional approaches.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;45 million Americans are not scoreable through traditional bureau data — addressing the thin-file problem requires non-traditional data sources, not better models on the same data&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Alternative Data: What Works and What Does Not
&lt;/h2&gt;

&lt;p&gt;Cash flow data — transaction-level bank account data showing income deposits, recurring payments, and spending patterns — is the most consistently useful alternative data source for credit risk in consumer lending. It is highly predictive of ability to pay, has reasonable coverage through open banking integrations, and is relatively clean compared to other alternative data sources. Cash flow features (income consistency, average balance, payment-to-income ratios) add meaningful lift for thin-file borrowers on top of bureau data.&lt;/p&gt;

&lt;p&gt;Rent payment history, when available and reliably reported, is also useful — a demonstrated track record of on-time rent payments is predictive of future credit performance. The coverage problem is significant: most rent payments are not reported to any bureau, and the coverage of rent payment data from alternative providers is incomplete and variable by geography.&lt;/p&gt;

&lt;p&gt;Data sources that have shown less consistent value: social media signals, education and employment data from non-traditional sources, and 'character' signals derived from digital behaviour. These data types often introduce fair lending risk (proxies for protected characteristics appear in unexpected places) without delivering equivalent predictive lift. Several lenders have retreated from these approaches after fair lending testing revealed disparate impact.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cash flow data is the most consistently predictive alternative data source. Character and digital behaviour signals introduce fair lending risk without reliable predictive lift&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Recency Problem
&lt;/h2&gt;

&lt;p&gt;Credit risk models trained on pre-2020 data reflect credit behaviour in a low-interest-rate, low-unemployment economic environment. Models trained primarily on 2020–2022 data reflect an unprecedented economic disruption period with stimulus payments, forbearance programs, and unusual payment behaviour. Neither training period is a reliable guide to current credit risk in a normalising interest rate environment.&lt;/p&gt;

&lt;p&gt;The practical implication: models trained on data more than two to three years old should be retested against recent performance data before being treated as reliable. Performance metrics that looked stable during model development may have degraded significantly as the economic environment shifted.&lt;/p&gt;

&lt;p&gt;This is not a new problem — economic cycle sensitivity in credit models has been understood for decades. It became more acute after 2020 because the economic disruption was severe enough to make pre-crisis training data significantly less predictive of post-crisis behaviour. Lenders whose models date from 2018 or earlier and have not been recalibrated against post-2022 performance data are running on models whose accuracy may be materially worse than their validation metrics suggest.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Models trained on pre-2020 data should be retested against recent performance data — the economic environment shift has degraded accuracy in ways validation metrics from that period do not reflect&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Model Validation in a Regulated Environment
&lt;/h2&gt;

&lt;p&gt;For banks and supervised lenders, credit risk models are subject to model risk management requirements under SR 11-7. The validation requirement is not just that the model performs well by internal metrics — it is that the model has been independently validated, the validation covers conceptual soundness and outcome analysis, limitations are documented, and ongoing monitoring is in place.&lt;/p&gt;

&lt;p&gt;For the models with the most regulatory scrutiny — models used in underwriting decisions for consumer credit at scale — the validation needs to cover fair lending analysis as a standard component. A model that validates well on predictive performance metrics but has not been tested for disparate impact is not ready for production at a regulated lender.&lt;/p&gt;

&lt;p&gt;The practical implication for AI/ML models: more complex models require more documentation and a more sophisticated validation function. The validation team needs to understand the feature engineering, the training data, the hyperparameter selection rationale, and the limitations of the model well enough to write a challenge that addresses each. This is achievable but it requires planning — presenting a finished ML model to model risk management without having engaged them in the development process is a common reason for long validation timelines.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Engaging model risk management during development, not at the end of it, is the single most effective way to avoid long validation delays&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cobuildx.ai/blog/credit-risk-model-data-quality-2025" rel="noopener noreferrer"&gt;CobuildX blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>fintech</category>
      <category>machinelearning</category>
      <category>compliance</category>
    </item>
    <item>
      <title>Fraud Detection: Why Your Rules Engine Has a Ceiling</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Thu, 08 Oct 2026 11:01:13 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/fraud-detection-why-your-rules-engine-has-a-ceiling-l9n</link>
      <guid>https://dev.to/cobuildx-ai/fraud-detection-why-your-rules-engine-has-a-ceiling-l9n</guid>
      <description>&lt;p&gt;&lt;em&gt;Rules-based fraud detection is fast to deploy and easy to explain. It also has a ceiling: fraudsters adapt to rules, false positive rates are hard to reduce, and new fraud patterns take time to codify. ML adds a measurable lift above that ceiling — here is what that looks like in practice.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most financial institutions run fraud detection on a rules engine. Block transactions over a certain dollar amount to unusual recipients. Flag velocity patterns above threshold. Decline cards used in multiple geographies within a short window. These rules work. They catch a significant fraction of fraud and they have the advantage of being fast to implement, easy to explain to regulators, and transparent to the operations team. They also have a ceiling that most institutions have already hit.&lt;/p&gt;

&lt;p&gt;A rules engine is a static representation of known fraud patterns. Fraudsters adapt to it. Once a particular attack pattern triggers enough rule-based blocks, the fraud operation modifies its behaviour to stay below the thresholds — lower transaction amounts, slower velocity, more geographically plausible patterns. The rules that were catching fraud last year catch less of it this year, and adding new rules to close the gap increases false positives on legitimate transactions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; Rules catch the fraud patterns you have already seen and codified. ML catches the patterns that are statistically anomalous but have not yet been written into a rule — which is where new fraud appears.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Every time we add a new rule to close a fraud vector, our false positive rate goes up and our customer service team starts getting calls about blocked legitimate transactions."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Rules-Based Detection Does Well
&lt;/h2&gt;

&lt;p&gt;Rules-based fraud detection has genuine strengths that explain why it remains the foundation of most fraud stacks.&lt;/p&gt;

&lt;p&gt;It is deterministic and auditable. When a transaction is blocked, the operations team can tell the customer exactly which rule triggered the decline. This transparency matters for customer relations and for regulatory examinations.&lt;/p&gt;

&lt;p&gt;It is fast. Rule evaluation is computationally trivial — microseconds per transaction. At the transaction volumes that large financial institutions process, latency matters.&lt;/p&gt;

&lt;p&gt;It handles known, well-defined attack patterns reliably. Card-not-present fraud with high velocity on new accounts, account takeover with credential stuffing patterns, check kiting with specific inter-account timing signatures — these patterns can be codified into rules that catch them with high precision.&lt;/p&gt;

&lt;p&gt;Rules are also the right tool for hard business constraints that are not about statistical patterns: geographic restrictions, product eligibility requirements, regulatory holds. These belong in a rules engine regardless of how sophisticated the ML layer is.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Rules are the right tool for hard business constraints and well-defined known patterns — they are not the right tool for detecting fraud patterns that have not been codified yet&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Where the Ceiling Is
&lt;/h2&gt;

&lt;p&gt;The ceiling on rules-based detection shows up in three ways.&lt;/p&gt;

&lt;p&gt;First, new fraud patterns. When a new attack vector appears — a new synthetic identity structure, a new card-not-present exploit, a new account funding scheme — it takes time to detect the pattern, analyse it, write the rule, test it, and deploy it. During that window, fraud losses accumulate. The detection gap between when new fraud appears and when a rule catches it is typically weeks to months.&lt;/p&gt;

&lt;p&gt;Second, the precision-recall tradeoff. Adding rules to catch more fraud always increases false positives on legitimate transactions. At some point, the marginal fraud caught by the next rule is less costly than the customer friction and manual review cost of the additional false positives it creates. Most mature rules engines are already at or past this point.&lt;/p&gt;

&lt;p&gt;Third, adversarial adaptation. Fraud rings that are large enough to observe their own block rates modify their behaviour to avoid triggered rules. This is well-documented in card fraud, account opening fraud, and money movement fraud. Rules that were effective eighteen months ago are less effective today because the fraud behaviour has changed to avoid them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;New patterns, false positive ceilings, and adversarial adaptation are the three vectors where rules-based detection consistently underperforms&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What ML Adds
&lt;/h2&gt;

&lt;p&gt;ML fraud detection models — gradient boosting classifiers, neural networks, graph-based anomaly detection — catch fraud through a different mechanism than rules. They learn the statistical profile of legitimate transactions and flag deviations from that profile, without requiring those deviations to match a specific predefined pattern.&lt;/p&gt;

&lt;p&gt;This means they can catch fraud that has not been codified into a rule yet. A new synthetic identity scheme may not trigger any existing rule, but it will deviate from the statistical profile of legitimate new accounts in ways that a well-trained model detects. The fraudsters would need to perfectly mimic the full behavioural and demographic distribution of legitimate customers — not just avoid specific thresholds — to evade a model-based detector.&lt;/p&gt;

&lt;p&gt;The measurable lift of adding ML on top of rules varies significantly by institution and fraud type. In card fraud, lift of 15–30% reduction in fraud losses at equivalent false positive rates is commonly reported. In account opening fraud and synthetic identity fraud, the lift can be larger because these fraud types are harder to codify into rules.&lt;/p&gt;

&lt;p&gt;ML does not replace the rules engine. The two work better together than either does alone. Hard constraints belong in rules. Statistical anomaly detection belongs in the ML layer. Most production fraud stacks run both.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;ML adds 15–30% fraud loss reduction at equivalent false positive rates by catching statistical anomalies that rules cannot codify — it works alongside rules, not instead of them&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Model Drift in an Adversarial Environment
&lt;/h2&gt;

&lt;p&gt;Fraud detection models face a challenge that most ML models do not: the distribution they are trained on is actively manipulated by the adversary once the model is deployed.&lt;/p&gt;

&lt;p&gt;A churn prediction model trained on last year's customers is less accurate this year because customer behaviour naturally evolved. A fraud model trained on last year's fraud patterns is less accurate this year partly because fraud behaviour naturally evolved, and partly because fraud operators specifically adapted to avoid the model's detection patterns.&lt;/p&gt;

&lt;p&gt;This means fraud models need more frequent retraining than most ML applications — quarterly at minimum for high-volume card fraud, more frequently if new attack vectors are appearing. It also means the training data pipeline needs to be maintained carefully: new labelled fraud examples need to be incorporated quickly, and the labelling process (which requires investigations and chargebacks to confirm fraud) needs to be timely enough to produce useful training signal.&lt;/p&gt;

&lt;p&gt;Monitoring for model drift in fraud detection should track both the false positive rate (to catch model degradation on legitimate transactions) and the fraud detection rate (to catch model degradation on fraud). Both can degrade independently if the fraud distribution shifts.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Fraud models require more frequent retraining than most ML applications because the adversary actively adapts — quarterly retraining is a minimum, not a target&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cobuildx.ai/blog/fraud-detection-rules-engine-ml" rel="noopener noreferrer"&gt;CobuildX blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>fintech</category>
      <category>machinelearning</category>
      <category>compliance</category>
    </item>
    <item>
      <title>Document Processing in Loan Origination: Where IDP Projects Stall</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Thu, 08 Oct 2026 10:59:58 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/document-processing-in-loan-origination-where-idp-projects-stall-2g2k</link>
      <guid>https://dev.to/cobuildx-ai/document-processing-in-loan-origination-where-idp-projects-stall-2g2k</guid>
      <description>&lt;p&gt;&lt;em&gt;Intelligent document processing handles pay stubs, bank statements, and tax returns well under controlled conditions. The 20% exception rate, downstream system integration, and confidence threshold design are where most production deployments get complicated.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Loan origination involves processing large volumes of documents — pay stubs, bank statements, W-2s, tax returns, bank statements, rent rolls, business financials. The manual review process is slow, expensive, and error-prone. Intelligent document processing can automate significant portions of it. The business case is clear and the technology is mature enough to deploy. Where these projects consistently run into difficulty is the exception handling, the integration, and the operational model needed to manage the cases the AI cannot handle confidently.&lt;/p&gt;

&lt;p&gt;IDP pilots demonstrate high accuracy on clean, well-formatted documents from a controlled sample. Production document volumes include faxed photocopies of handwritten notes, bank statements from forty different institutions with different formats, pay stubs from payroll systems that were last updated in 2009, and documents that are technically legible but structured in ways the model has not seen before. The accuracy numbers change.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; The 80% that IDP handles confidently reduces costs significantly. The 20% that it handles poorly requires a human review queue that needs to be designed, staffed, and managed — and if that queue is not planned for, it creates more work than the automation saves.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"The demo showed 95% accuracy on our documents. In production, 30% of submissions were being routed to the exception queue within the first month."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why Each Document Type Is Harder Than It Looks
&lt;/h2&gt;

&lt;p&gt;Pay stubs look standardised but are produced by hundreds of different payroll platforms, each with its own layout, field naming conventions, and data formatting. A model trained primarily on ADP and Paychex output encounters Gusto, Square Payroll, or a homemade Excel template and performance degrades. The fields are all there — gross pay, YTD earnings, deductions — but their position, labelling, and format vary enough to challenge extraction models trained on limited layout diversity.&lt;/p&gt;

&lt;p&gt;Bank statements have similar issues compounded by the sheer number of issuing institutions. Major banks have consistent statement formats that are well-represented in training data. Credit unions, regional banks, and online-only institutions may have formats the model has rarely or never seen. International bank statements add language and number formatting complexity.&lt;/p&gt;

&lt;p&gt;Tax returns (1040, 1120S, Schedule C) are more standardised than bank statements, but handwritten forms, amended returns, and older year forms create edge cases. More significantly, the fields that matter most for underwriting — self-employment income, depreciation add-backs, business use of home — require reading across multiple schedules and applying accounting logic, not just field extraction.&lt;/p&gt;

&lt;p&gt;Rent rolls for commercial real estate loans are the hardest category: they are typically Excel files or PDFs with no standard format, produced by property managers with highly variable approaches to data organisation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Format diversity, not document complexity, is the primary challenge — models trained on major institutions encounter edge cases at rates higher than demo conditions suggest&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Designing for the Exception Rate
&lt;/h2&gt;

&lt;p&gt;Every IDP deployment has an exception rate — the percentage of documents that the model routes to human review because its confidence falls below threshold. In a well-designed deployment, this is a feature, not a failure: the model knows what it does not know and escalates appropriately rather than producing a confident wrong extraction.&lt;/p&gt;

&lt;p&gt;The exception rate design decision is a threshold calibration problem. Setting the confidence threshold high routes more documents to human review but reduces extraction errors that make it downstream. Setting it low reduces the review queue but allows more errors through. The right threshold depends on the cost of a missed extraction (an underwriting decision made on wrong income data) versus the cost of the human review (processor time and cycle time impact).&lt;/p&gt;

&lt;p&gt;The exception queue also needs to be designed as a workflow, not an afterthought. Who reviews exceptions? What tools do they use? How does a corrected extraction feed back as a training signal to improve the model over time? How is the exception rate monitored and how does it trigger model retraining? These questions need to be answered before deployment, not after the queue fills up.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Design the exception queue as a workflow from the start — the 20% that goes to human review determines whether the overall system reduces costs or increases them&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Downstream System Integration
&lt;/h2&gt;

&lt;p&gt;IDP extracts structured data from documents. That data needs to go somewhere — typically a loan origination system (LOS) where it pre-populates the application, a verification system where it is compared to borrower-stated values, or a data repository where it feeds underwriting calculations.&lt;/p&gt;

&lt;p&gt;The integration with the LOS is often the longest part of the project. Most major LOS platforms (Encompass, Blend, nCino) have APIs, but the field mapping between IDP output and LOS data fields requires detailed mapping work. Edge cases abound: the IDP system extracts monthly net pay; the LOS field expects annual gross. The bank statement extraction returns a list of transaction-level deposits; the LOS wants average monthly deposits as a single number.&lt;/p&gt;

&lt;p&gt;Audit trail requirements add another integration consideration. For regulatory purposes, lenders need to be able to show what data was extracted, from which document, by which system, with what confidence, and what human review occurred. This audit trail needs to be stored somewhere and accessible for examination. Building it in after the fact is significantly more work than designing it from the start.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;LOS field mapping and audit trail design are often longer than the model development — scope them explicitly upfront&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What a Production-Ready IDP System Requires
&lt;/h2&gt;

&lt;p&gt;A production-ready IDP system in loan origination is not just a document extraction model. It is the model plus the confidence threshold logic, plus the exception queue workflow, plus the LOS integration, plus the audit trail, plus the feedback loop from human corrections to model retraining, plus the monitoring that tracks extraction accuracy over time and flags model drift as new document formats appear.&lt;/p&gt;

&lt;p&gt;The extraction model is typically 20–30% of the total implementation work in a well-scoped project. The remainder is the surrounding infrastructure. Projects that scope only the model and discover the infrastructure requirements mid-deployment are the ones that overrun timelines and budgets.&lt;/p&gt;

&lt;p&gt;The ROI case is real and significant — processing costs for a fully automated document reduce by 70–80% versus manual review for the 80% of documents the model handles confidently. The business case holds even after accounting for the exception queue. It just requires scoping the full system, not just the model.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The extraction model is 20–30% of the work — the exception queue, LOS integration, audit trail, and monitoring are the rest&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cobuildx.ai/blog/intelligent-document-processing-loan-origination" rel="noopener noreferrer"&gt;CobuildX blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>fintech</category>
      <category>machinelearning</category>
      <category>compliance</category>
    </item>
    <item>
      <title>AI in Underwriting: What Regulators Actually Care About</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Thu, 08 Oct 2026 10:59:23 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/ai-in-underwriting-what-regulators-actually-care-about-35k5</link>
      <guid>https://dev.to/cobuildx-ai/ai-in-underwriting-what-regulators-actually-care-about-35k5</guid>
      <description>&lt;p&gt;&lt;em&gt;Lenders deploying AI in underwriting face a compliance landscape that shapes what models can be used, what documentation is required, and how adverse action notices must work. Here is what the regulatory framework actually requires and what you can deploy today.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;AI underwriting models can assess creditworthiness faster, more consistently, and — when built carefully — more accurately than traditional scorecards. Lenders know this. The compliance and legal teams know it too, and they have a set of questions that need answers before any AI model touches a credit decision. Those questions are not obstacles to deploying AI in underwriting. They are a framework that, once understood, makes it straightforward to separate what is deployable today from what requires more work.&lt;/p&gt;

&lt;p&gt;Most AI underwriting projects stall not because the model does not work but because the compliance path was not scoped upfront. Model development, validation, and business user testing proceed for six to nine months — and then the model sits in legal review because the explainability requirements, the adverse action notice process, and the fair lending testing were not designed in from the start.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; Regulatory requirements for AI in underwriting are not ambiguous. ECOA, the Fair Housing Act, SR 11-7 model risk management guidance, and the relevant CFPB guidance give a reasonably clear picture of what is required. The compliance path is knowable upfront — the projects that get stuck are the ones that did not engage it early.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"The model was excellent. We spent four months in legal trying to figure out how to write the adverse action notice."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Adverse Action Notice Requirement
&lt;/h2&gt;

&lt;p&gt;Under ECOA and Regulation B, lenders must provide applicants who are denied credit — or offered less favourable terms — with a written statement of the specific reasons for the adverse action. This requirement applies regardless of what type of model made the decision. A black-box neural network that cannot produce interpretable reason codes does not satisfy the adverse action notice requirement.&lt;/p&gt;

&lt;p&gt;For traditional scorecard models, adverse action reasons are generated directly from the score factors — the variables that contributed most negatively to the applicant's score. For complex ML models, producing equivalent reason codes requires either post-hoc explanation methods (SHAP values, LIME) or designing the model architecture to produce interpretable outputs.&lt;/p&gt;

&lt;p&gt;SHAP-based reason codes are increasingly accepted by compliance teams as a defensible approach, but they require validation. The reason codes need to be consistent — the same input should produce the same reason codes across runs — and they need to reflect the actual model mechanics rather than being post-hoc rationalisations that do not match the model's actual decision logic. Both of these properties require testing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Adverse action notices require specific reasons for the decision — post-hoc explanation methods like SHAP are increasingly accepted but need to be validated for consistency and accuracy&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Fair Lending: What Testing Is Required
&lt;/h2&gt;

&lt;p&gt;ECOA and the Fair Housing Act prohibit discrimination in credit decisions on the basis of race, colour, national origin, sex, religion, marital status, age, and other protected characteristics. This applies to AI models.&lt;/p&gt;

&lt;p&gt;The fair lending testing requirement for AI models is not that the model cannot use protected characteristics as inputs — that is a floor, not the full requirement. The actual requirement is disparate impact analysis: does the model produce materially different approval rates or pricing outcomes across protected groups that cannot be justified by legitimate credit risk differences?&lt;/p&gt;

&lt;p&gt;Fair lending testing should be run during model development, not after. If disparate impact is detected during development, the model can be adjusted — through feature selection, recalibration, or architectural changes — before deployment. If it is detected during regulatory examination after deployment, the remediation is more expensive and the reputational risk is significant.&lt;/p&gt;

&lt;p&gt;The specific statistical tests used for disparate impact analysis in lending — the 80% rule, regression-based control methods, the Bayesian Improved Surname Geocoding (BISG) proxy methodology for race and ethnicity — are well-documented. Fair lending review should be designed into the model validation process, not added as a final gate.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Fair lending testing belongs in the development process, not at the end of it — detecting disparate impact early is cheaper and safer than detecting it in an exam&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  SR 11-7 Model Risk Management
&lt;/h2&gt;

&lt;p&gt;SR 11-7 is the Federal Reserve and OCC's guidance on model risk management, and it applies to AI underwriting models at banks and other supervised institutions. The core requirements: models must be validated by a function independent of the model development team, documentation must cover model purpose, methodology, data, limitations, and performance metrics, and models must be subject to ongoing monitoring and periodic revalidation.&lt;/p&gt;

&lt;p&gt;For AI models specifically, SR 11-7 requires that the model validation team understand the model well enough to challenge it. This is harder for complex ML models than for traditional scorecards. Validators need to understand the training data, the feature engineering, the hyperparameter selection process, and the performance metrics used to evaluate the model — and document that they have assessed each for soundness.&lt;/p&gt;

&lt;p&gt;The practical implication: complex ML models require more documentation and a more sophisticated validation team than traditional models. This is not a reason to avoid them, but it is a reason to involve model risk management early in the development process rather than presenting them with a finished model for validation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;SR 11-7 validation requires the ability to challenge the model — complex ML models require more documentation and more sophisticated validators than traditional scorecards&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What You Can Deploy Today
&lt;/h2&gt;

&lt;p&gt;Given this framework, the question is which AI approaches fit within the compliance requirements without requiring regulatory engagement beyond standard model validation.&lt;/p&gt;

&lt;p&gt;Gradient boosting models (XGBoost, LightGBM) with SHAP-based reason codes are the current sweet spot for lenders who want meaningfully better predictive performance than traditional logistic regression scorecards without taking on interpretability risk they cannot manage. These models are significantly more predictive than scorecards on most credit datasets, produce consistent SHAP explanations, and have a validation pathway that most model risk management teams can work with.&lt;/p&gt;

&lt;p&gt;Logistic regression models with well-engineered features remain a defensible choice for regulated lenders who want maximum interpretability and a simpler validation process. The performance gap versus gradient boosting depends on the dataset, but for standard consumer lending use cases the gap is often smaller than the implementation complexity difference.&lt;/p&gt;

&lt;p&gt;Deep learning models and LLM-based approaches require more care. They can produce excellent predictive performance, but the interpretability pathway, the validation approach, and the adverse action reason code methodology all require more work to make defensible. They are deployable, but they require more upfront investment in the compliance infrastructure.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gradient boosting with SHAP reason codes is the current practical standard for regulated lenders wanting ML performance without interpretability complexity&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cobuildx.ai/blog/ai-underwriting-compliance-explainability" rel="noopener noreferrer"&gt;CobuildX blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>fintech</category>
      <category>machinelearning</category>
      <category>compliance</category>
    </item>
    <item>
      <title>Scheduling in High-Mix Low-Volume Manufacturing: Why Rules Break</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Thu, 08 Oct 2026 10:58:46 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/scheduling-in-high-mix-low-volume-manufacturing-why-rules-break-30hn</link>
      <guid>https://dev.to/cobuildx-ai/scheduling-in-high-mix-low-volume-manufacturing-why-rules-break-30hn</guid>
      <description>&lt;p&gt;&lt;em&gt;High-mix low-volume manufacturing scheduling is one of the harder operational problems in industry. ERP scheduling modules are built for repetitive production. Custom rules break under complexity. Here is what AI scheduling actually delivers and what it does not.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;High-mix low-volume manufacturing — job shops, contract manufacturers, specialty fabricators — has a scheduling problem that standard ERP tools were not designed to solve. When you are producing hundreds of different part numbers in small quantities, with sequence-dependent setup times, shared constrained resources, and customer-specific due dates, the scheduling problem becomes combinatorially complex fast. Rules that work for fifty active jobs fall apart at five hundred.&lt;/p&gt;

&lt;p&gt;ERP production scheduling modules are built around the assumptions of repetitive manufacturing — standard routings, predictable cycle times, stable demand. HMLV environments violate all of these assumptions regularly. The result: schedulers spend most of their time manually adjusting the ERP schedule, working around its recommendations rather than relying on them. The schedule is a starting point, not an operating plan.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; The scheduling problem in HMLV manufacturing is not that the rules are wrong — it is that the problem is too large and too dynamic for any static rule set to handle well. The complexity requires optimisation, not just sequencing.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"The ERP schedule is what we show customers. The actual production sequence is what the floor supervisor decides every morning."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Makes HMLV Scheduling Hard
&lt;/h2&gt;

&lt;p&gt;Several characteristics of high-mix low-volume environments combine to make scheduling genuinely difficult.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sequence-dependent setup times.&lt;/strong&gt; On a CNC machining centre or a paint line, the setup time between consecutive jobs depends on which job was run immediately before. Switching from a long part to a short part may require a fixture change that takes two hours; switching between two similar parts may take fifteen minutes. Optimal sequencing minimises total setup time across the shift — but computing the optimal sequence across hundreds of jobs and dozens of machines is a hard combinatorial problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shared constrained resources.&lt;/strong&gt; Critical resources — a specific CNC, a specialist operator, a testing station — create bottlenecks that cascade through the schedule. A job that is delayed at a bottleneck delays every subsequent job on that part's routing. The schedule needs to account for resource contention across all jobs simultaneously, not job by job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dynamic arrivals.&lt;/strong&gt; New jobs arrive, existing jobs change priority, customers call with expedite requests, and machines break down. The schedule that was optimal this morning is out of date by noon. Rescheduling in response to every disruption is computationally expensive and disruptive to the floor.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Sequence-dependent setups, shared bottleneck resources, and dynamic disruptions combine to make HMLV scheduling a problem that static rules cannot handle reliably&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Where ERP and APS Fall Short
&lt;/h2&gt;

&lt;p&gt;Most ERP systems use a variant of finite capacity scheduling or simple priority rules (earliest due date, shortest processing time) to generate production sequences. These approaches are fast to compute and easy to understand, but they do not optimise across the full problem — they make locally reasonable decisions that do not account for downstream effects.&lt;/p&gt;

&lt;p&gt;Advanced Planning and Scheduling (APS) tools are more sophisticated and can handle sequence-dependent setup times and resource contention better than standard ERP. The limitations: they require accurate master data (routing times, setup matrices, resource capacity), they struggle with frequent reschedules in highly dynamic environments, and they are expensive to configure and maintain.&lt;/p&gt;

&lt;p&gt;The practical result in many HMLV shops: the APS or ERP schedule is used as a capacity load view — a rough guide to whether the shop is overloaded for the week — while the actual daily sequencing decisions are made by experienced floor supervisors using rules of thumb developed over years on the floor. This is not a failure of the floor team. It is a reasonable response to tools that are not fit for the actual problem complexity.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;APS tools improve on ERP but still require accurate master data and struggle with dynamic environments — most HMLV shops are scheduling around them, not with them&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What AI Scheduling Delivers
&lt;/h2&gt;

&lt;p&gt;AI scheduling approaches — typically reinforcement learning, genetic algorithms, or ML-enhanced heuristics — tackle the HMLV scheduling problem differently from rules or conventional optimisation.&lt;/p&gt;

&lt;p&gt;Reinforcement learning agents learn scheduling policies by simulating millions of scheduling scenarios and learning which sequencing decisions lead to better outcomes (on-time delivery, utilisation, total setup time). Once trained, they can generate schedules quickly enough to reschedule in near real-time as disruptions occur.&lt;/p&gt;

&lt;p&gt;ML-enhanced heuristics use historical production data to learn which priority rules work best under which conditions — time of day, resource availability, mix of job types in queue — and apply different rules dynamically rather than using a single rule universally.&lt;/p&gt;

&lt;p&gt;Realistic improvements in well-implemented HMLV scheduling AI: 10–20% reduction in total setup time through better sequencing, 5–15% improvement in on-time delivery by accounting for bottleneck contention earlier in the planning horizon, and meaningfully faster response to disruptions.&lt;/p&gt;

&lt;p&gt;These gains require accurate data: reliable routing times, up-to-date setup matrices, and real-time machine status. Scheduling AI does not fix data quality problems — it amplifies them. A model scheduling against inaccurate routing times will produce schedules that fail on the floor in new ways.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;10–20% setup time reduction and 5–15% on-time delivery improvement are realistic — with accurate routing data and setup matrices as the prerequisite&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Constraint Modelling Problem
&lt;/h2&gt;

&lt;p&gt;Scheduling AI is only as useful as its model of the constraints it is optimising within. In HMLV manufacturing, the relevant constraints are numerous and often tacit.&lt;/p&gt;

&lt;p&gt;Formal constraints — machine capacity, operator certification requirements, material availability — are usually captured somewhere in the ERP or production system, though often incompletely. Tacit constraints — this machine runs hot and needs a cooldown between long jobs, this operator works better in the morning, this customer always expedites so we buffer their jobs — live in the floor supervisor's head.&lt;/p&gt;

&lt;p&gt;Capturing the tacit constraints is a requirements gathering exercise, not a data science problem. It requires time with the people who actually run the floor, not just the data they generate. AI scheduling that ignores these constraints produces schedules that look optimal by the formal criteria and fail in practice because they violate constraints the model did not know existed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tacit scheduling constraints — the rules the floor supervisor knows but nobody wrote down — need to be captured explicitly before the model can be trusted&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Making the Scheduler Useful to the Floor
&lt;/h2&gt;

&lt;p&gt;The final consideration is one that determines whether any scheduling improvement gets sustained: the floor team needs to trust the output enough to follow it.&lt;/p&gt;

&lt;p&gt;Scheduling tools that present an optimal sequence with no explanation for why jobs are ordered the way they are produce floor supervisors who override the sequence and revert to their existing approach. Schedulers that surface the reasoning — this job is prioritised because it is the only one this week that uses the bottleneck fixture, this setup sequence minimises changeover time across the afternoon — give supervisors the context to evaluate the recommendation rather than just accept or reject it.&lt;/p&gt;

&lt;p&gt;The goal is not to automate the floor supervisor out of the scheduling decision. It is to give them a starting point that accounts for more complexity than they can hold in their head, with enough transparency to apply their tacit knowledge on top of it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Schedulers that explain their reasoning get used. Schedulers that present opaque outputs get overridden&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cobuildx.ai/blog/high-mix-low-volume-scheduling-ai" rel="noopener noreferrer"&gt;CobuildX blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>manufacturing</category>
      <category>machinelearning</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Predictive Maintenance: The Gap Between the Pilot and Production</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Thu, 08 Oct 2026 10:58:10 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/predictive-maintenance-the-gap-between-the-pilot-and-production-1pef</link>
      <guid>https://dev.to/cobuildx-ai/predictive-maintenance-the-gap-between-the-pilot-and-production-1pef</guid>
      <description>&lt;p&gt;&lt;em&gt;Predictive maintenance pilots succeed regularly. Production deployments are harder. The difference is usually not the model — it is asset coverage, alert fatigue, maintenance workflow integration, and the trust gap that builds when the first false positives arrive.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Predictive maintenance is one of the most piloted AI applications in manufacturing — and one of the most frequently stalled between pilot and production. The concept is straightforward enough: detect patterns in sensor data that precede equipment failure, alert the maintenance team early enough to schedule a planned repair instead of responding to an unplanned breakdown. The ROI case is clear. The pilot usually works. The full deployment frequently does not.&lt;/p&gt;

&lt;p&gt;Pilots are run on selected assets, with dedicated engineering support, in controlled conditions. Production deployments have to work across the full asset base, integrate with existing maintenance workflows, generate alerts that maintenance planners trust enough to act on, and survive the realities of sensor data quality, shift changes, and competing priorities. The gap between those two environments is where most predictive maintenance programs stall.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; The model is rarely the failure point. Alert fatigue, workflow integration, and the trust gap created by the first wave of false positives are what prevent pilot-proven technology from reaching production scale.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"The pilot ran for three months and the model was excellent. Then we deployed it across all forty pumps and maintenance stopped looking at the dashboard within six weeks."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why Pilots Succeed
&lt;/h2&gt;

&lt;p&gt;Predictive maintenance pilots are usually set up for success. Asset selection is deliberate — engineers choose the assets with the best sensor coverage, the clearest failure history, and the highest maintenance cost. The team running the pilot is engaged and attentive. When the model generates an alert, someone investigates it. When the alert proves correct, it gets documented. When it proves incorrect, the team discusses why and adjusts.&lt;/p&gt;

&lt;p&gt;This is not representative of production conditions. In production, the model runs across all assets. The maintenance team has a full workload and limited time to investigate every alert. No one is specifically assigned to track model performance. False positives accumulate without the immediate feedback loop that keeps the pilot honest.&lt;/p&gt;

&lt;p&gt;The transition plan needs to account for this difference explicitly. A pilot success is necessary but not sufficient evidence that a production deployment will work.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pilot conditions are optimised for success. Production conditions are not — the transition plan needs to bridge that gap deliberately&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Asset Coverage and Prioritisation
&lt;/h2&gt;

&lt;p&gt;A common mistake in predictive maintenance programs is trying to cover the full asset base from the start. Most plants have hundreds or thousands of assets. Instrumenting all of them, building models for all of them, and deploying alerts across all of them simultaneously is a scope that almost always exceeds the capacity of the team running the program.&lt;/p&gt;

&lt;p&gt;A better approach: start with the twenty or thirty assets that account for the majority of unplanned downtime cost. Not the assets that are easiest to model, or the ones that already have sensor coverage, but the ones where a successful prediction would have the highest value. Build the business case around those assets, deploy carefully, and expand to adjacent asset classes once the first cohort is working reliably.&lt;/p&gt;

&lt;p&gt;This also gives the maintenance team time to develop the workflow habits that make predictive maintenance effective — reviewing alerts, scheduling condition-based interventions, and feeding back the outcome of each intervention to improve model accuracy.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Start with the twenty assets that account for most unplanned downtime cost, not the twenty easiest to model&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Alert Fatigue Problem
&lt;/h2&gt;

&lt;p&gt;Alert fatigue is the single most common reason predictive maintenance programs fail to reach production scale. When a model generates more alerts than the maintenance team can act on — or generates alerts that turn out to be wrong often enough to erode trust — the team stops responding to them. Once that happens, the program is functionally dead regardless of how good the underlying model is.&lt;/p&gt;

&lt;p&gt;The precision-recall tradeoff is directly relevant here. A model can be tuned to catch more failures (higher recall) at the cost of more false positives (lower precision), or to have fewer false positives at the cost of missing some failures. In most maintenance contexts, precision matters more than recall in the first year of deployment. A high-precision, low-recall model that generates five alerts per month — all of which are credible — builds more trust than a high-recall model that generates fifty alerts, forty of which do not result in a confirmed finding.&lt;/p&gt;

&lt;p&gt;The alert threshold should be set conservatively at first, with the explicit understanding that it will be relaxed as the team develops confidence in the system and as the model accumulates more data to learn from.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Set alert thresholds conservatively in the first year — building maintenance team trust matters more than catching every failure&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Workflow Integration
&lt;/h2&gt;

&lt;p&gt;Predictive maintenance alerts only create value if they change maintenance behaviour. That requires the alerts to reach the people who schedule maintenance, in a form they can act on, integrated with the tools they already use.&lt;/p&gt;

&lt;p&gt;An alert in a monitoring dashboard that the maintenance planner never opens is not a workflow change. An alert that flows into the CMMS (Computerised Maintenance Management System) — generating a work order recommendation that can be accepted, rejected, or deferred — is. The difference between these two integration approaches often determines whether the program persists past the first year.&lt;/p&gt;

&lt;p&gt;The CMMS integration also creates the feedback loop that improves the model over time. When a maintenance team accepts an alert-generated work order and finds a genuine problem during the inspection, that outcome gets recorded. When they reject an alert because nothing was found, that gets recorded too. This feedback is the training signal that improves precision and recall over time.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Alerts that flow into the CMMS as actionable work order recommendations create both workflow integration and the feedback loop that improves the model&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Realistic Year-One Success Looks Like
&lt;/h2&gt;

&lt;p&gt;A realistic target for a well-scoped first-year predictive maintenance program: 20–40% reduction in unplanned downtime on the target asset cohort, measured against the prior year baseline. Not zero unplanned failures — some failures will happen too fast for any prediction to be actionable, and some will be caused by failure modes not represented in the training data.&lt;/p&gt;

&lt;p&gt;The equally important year-one output: a working data pipeline from the historian to the prediction system, a proven alert-to-work-order workflow, a maintenance team that trusts the system enough to act on its recommendations, and a documented set of lessons about which asset types and failure modes the model handles well and which require further work. These are the infrastructure that makes year two faster and more valuable.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Year one delivers 20–40% reduction in unplanned downtime on the target assets and the infrastructure that makes year two faster&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cobuildx.ai/blog/predictive-maintenance-pilot-to-production" rel="noopener noreferrer"&gt;CobuildX blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>manufacturing</category>
      <category>machinelearning</category>
      <category>datascience</category>
    </item>
  </channel>
</rss>
