<?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: Sakthikumaran Navakumar</title>
    <description>The latest articles on DEV Community by Sakthikumaran Navakumar (@sakthicodes22).</description>
    <link>https://dev.to/sakthicodes22</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%2F3765235%2Fe0f10403-ca82-4885-8a49-56f60d5aac0e.jpeg</url>
      <title>DEV Community: Sakthikumaran Navakumar</title>
      <link>https://dev.to/sakthicodes22</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sakthicodes22"/>
    <language>en</language>
    <item>
      <title>The Orchestrator - How Native Federation v4 Decides What Loads (and What Changed in 4.8.0)</title>
      <dc:creator>Sakthikumaran Navakumar</dc:creator>
      <pubDate>Wed, 21 Oct 2026 02:07:45 +0000</pubDate>
      <link>https://dev.to/sakthicodes22/the-orchestrator-how-native-federation-v4-decides-what-loads-and-what-changed-in-480-36dp</link>
      <guid>https://dev.to/sakthicodes22/the-orchestrator-how-native-federation-v4-decides-what-loads-and-what-changed-in-480-36dp</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 5 of an 8-part series on Native Federation v4 for enterprise architecture teams. New to the series? Start with &lt;a href="https://dev.to/sakthicodes22/the-mental-model-revisited-why-native-federation-exists-and-what-changed-in-v4-1klg"&gt;Part 1: The Mental Model, Revisited&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/sakthicodes22/the-classic-v3-runtime-what-it-got-right-and-where-it-hit-its-limit-1l23"&gt;Part 4&lt;/a&gt; ended with three open questions about the v3 Classic Runtime:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How do you share a library across a &lt;strong&gt;range&lt;/strong&gt; of versions, instead of demanding exact matches?&lt;/li&gt;
&lt;li&gt;How do you make &lt;code&gt;singleton&lt;/code&gt; and &lt;code&gt;strictVersion&lt;/code&gt; actually do something?&lt;/li&gt;
&lt;li&gt;How do you avoid fetching everything again on every page load?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Orchestrator, &lt;code&gt;@softarc/native-federation-orchestrator&lt;/code&gt;, is the v4 runtime built to answer them (&lt;a href="https://native-federation.com/docs/v4/orchestrator/" rel="noopener noreferrer"&gt;Orchestrator docs&lt;/a&gt;). This article shows how it decides what loads, using the same example as &lt;a href="https://dev.to/sakthicodes22/the-classic-v3-runtime-what-it-got-right-and-where-it-hit-its-limit-1l23"&gt;Part 4&lt;/a&gt;, so you can compare the results directly. It also covers version &lt;strong&gt;4.8.0&lt;/strong&gt;, which changed how those decisions are made.&lt;/p&gt;

&lt;p&gt;As in &lt;a href="https://dev.to/sakthicodes22/the-classic-v3-runtime-what-it-got-right-and-where-it-hit-its-limit-1l23"&gt;Part 4&lt;/a&gt;, I ran the Orchestrator's real code (releases 4.7.0 and 4.8.0) against sample files, so every import map below is real output.&lt;/p&gt;

&lt;h2&gt;
  
  
  How you use it
&lt;/h2&gt;

&lt;p&gt;In Angular, you don't call the Orchestrator directly. The Angular adapter's &lt;code&gt;initFederation()&lt;/code&gt; wraps it and sets sensible defaults (&lt;a href="https://github.com/native-federation/angular-adapter/blob/main/src/index.ts" rel="noopener noreferrer"&gt;adapter source&lt;/a&gt;). Your &lt;code&gt;main.ts&lt;/code&gt; looks the same as in &lt;a href="https://dev.to/sakthicodes22/build-time-end-to-end-from-federationconfig-to-remoteentryjson-1980"&gt;Part 3&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The big change is what &lt;code&gt;initFederation()&lt;/code&gt; gives back. It &lt;strong&gt;returns&lt;/strong&gt; a &lt;code&gt;loadRemoteModule&lt;/code&gt; function, which you pass into your app (in Angular, usually through an &lt;code&gt;InjectionToken&lt;/code&gt;). In v3, you imported &lt;code&gt;loadRemoteModule&lt;/code&gt; globally from the runtime package. That global import still works in v4, but it's marked deprecated.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works, in four steps
&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%2Fcl0ffvdzhapkgmfj4s60.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%2Fcl0ffvdzhapkgmfj4s60.png" alt="The Orchestrator flow: main.ts calls initFederation. 1. Fetch the host's and every remote's remoteEntry.json, or reuse the cache. 2. Store them in the Orchestrator's caches. 3a. Decision 1: pick one winning version per shared library. 3b. Decision 2, new in 4.8.0: pick one serving build per package. 4. Write the import map. initFederation returns loadRemoteModule." width="560" height="1832"&gt;&lt;/a&gt;&amp;lt;!-- dev.to: upload d1-orchestrator-flow.png and replace the path above with its URL --&amp;gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fetch every &lt;code&gt;remoteEntry.json&lt;/code&gt;,&lt;/strong&gt; the host's and each remote's, or reuse them from the cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store them&lt;/strong&gt; in the Orchestrator's caches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide what to share.&lt;/strong&gt; This is the step the v3 Classic Runtime never had, and it involves two decisions:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Decision 1:&lt;/strong&gt; which &lt;strong&gt;version&lt;/strong&gt; of each shared library wins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decision 2 (new in 4.8.0):&lt;/strong&gt; which &lt;strong&gt;build&lt;/strong&gt; of that package everyone uses.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the import map&lt;/strong&gt; and return &lt;code&gt;loadRemoteModule&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Decision 1: choosing the version
&lt;/h2&gt;

&lt;p&gt;For each shared library, the Orchestrator follows a few simple rules (&lt;a href="https://native-federation.com/docs/v4/orchestrator/version-resolver/" rel="noopener noreferrer"&gt;version resolver&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Only &lt;code&gt;singleton&lt;/code&gt; libraries are negotiated.&lt;/strong&gt; A library with &lt;code&gt;singleton: false&lt;/code&gt; gives each remote its own copy, with no discussion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The host's version wins,&lt;/strong&gt; if the host uses the library.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If the host doesn't use it,&lt;/strong&gt; the Orchestrator picks the version that causes the fewest extra downloads. (You can change this to "newest version wins" with the &lt;code&gt;latestSharedExternal&lt;/code&gt; setting.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Then each remote is checked against the winner:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;If the winner fits the remote's &lt;code&gt;requiredVersion&lt;/code&gt;, the remote uses it.&lt;/li&gt;
&lt;li&gt;If it doesn't fit and &lt;code&gt;strictVersion: true&lt;/code&gt;, the remote gets its own copy.&lt;/li&gt;
&lt;li&gt;If it doesn't fit and &lt;code&gt;strictVersion: false&lt;/code&gt;, the remote uses the winner anyway.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the settings from &lt;a href="https://dev.to/sakthicodes22/build-time-end-to-end-from-federationconfig-to-remoteentryjson-1980"&gt;Part 3&lt;/a&gt; finally matter:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;v3 Classic Runtime&lt;/th&gt;
&lt;th&gt;v4 Orchestrator&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;singleton&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ignored&lt;/td&gt;
&lt;td&gt;Decides whether a library is negotiated at all&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;requiredVersion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ignored&lt;/td&gt;
&lt;td&gt;Decides whether a remote can use the winning version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;strictVersion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ignored&lt;/td&gt;
&lt;td&gt;Decides what happens when it can't: own copy (&lt;code&gt;true&lt;/code&gt;) or use the winner anyway (&lt;code&gt;false&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The only thing checked, and only for an exact match&lt;/td&gt;
&lt;td&gt;Used to pick the winner&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Same example, different result
&lt;/h2&gt;

&lt;p&gt;Let's take &lt;a href="https://dev.to/sakthicodes22/the-classic-v3-runtime-what-it-got-right-and-where-it-hit-its-limit-1l23"&gt;Part 4&lt;/a&gt;'s example again: the same &lt;code&gt;package.json&lt;/code&gt; files and the same &lt;code&gt;remoteEntry.json&lt;/code&gt; files. The host has Angular 20.1.4, &lt;code&gt;mfe1&lt;/code&gt; has 20.1.4, &lt;code&gt;mfe2&lt;/code&gt; has 20.1.5, and all three accept &lt;code&gt;^20.1.0&lt;/code&gt;. The host's build includes every Angular part the remotes use (more on why that matters in the next section).&lt;/p&gt;

&lt;p&gt;This is the import map the Orchestrator produced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"imports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@angular/core"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:4200/angular-core-AAA.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@angular/core/rxjs-interop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:4200/rxjs-interop-AAB.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rxjs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:4200/rxjs-BBB.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mfe1/./Component"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:4201/Component-CCC.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mfe2/./Component"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:4202/Component-FFF.js"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are no scopes at all. Every app uses the host's Angular, including &lt;code&gt;mfe2&lt;/code&gt;, because 20.1.4 fits its &lt;code&gt;^20.1.0&lt;/code&gt; range. Under the v3 Classic Runtime, &lt;code&gt;mfe2&lt;/code&gt; loaded its own Angular because 20.1.4 and 20.1.5 weren't an exact match.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/sakthicodes22/the-classic-v3-runtime-what-it-got-right-and-where-it-hit-its-limit-1l23"&gt;Part 4&lt;/a&gt;'s late &lt;code&gt;plugin&lt;/code&gt; remote, built with Angular 20.1.6, gets the same treatment. Added after startup, it only contributed its own component, and used the host's Angular and RxJS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"imports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"plugin/./Widget"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:4203/Widget-JJJ.js"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://dev.to/sakthicodes22/the-classic-v3-runtime-what-it-got-right-and-where-it-hit-its-limit-1l23"&gt;Part 4&lt;/a&gt;'s page had three copies of Angular. This one has one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 2: one build per package (4.8.0)
&lt;/h2&gt;

&lt;p&gt;To understand the 4.8.0 change, you need one more idea: &lt;strong&gt;a package has several parts.&lt;/strong&gt; Angular's &lt;code&gt;@angular/core&lt;/code&gt; package includes &lt;code&gt;@angular/core&lt;/code&gt;, &lt;code&gt;@angular/core/rxjs-interop&lt;/code&gt; and more. As &lt;a href="https://dev.to/sakthicodes22/build-time-end-to-end-from-federationconfig-to-remoteentryjson-1980"&gt;Part 3&lt;/a&gt; showed, each build only includes the parts its own code imports. So two builds of "Angular 20.1.4" can contain different files.&lt;/p&gt;

&lt;p&gt;That creates a problem. What if the host's Angular is missing a part a remote needs?&lt;/p&gt;

&lt;h3&gt;
  
  
  Before 4.8.0: borrowing missing parts
&lt;/h3&gt;

&lt;p&gt;Up to version 4.7.x, the Orchestrator &lt;strong&gt;borrowed&lt;/strong&gt; the missing part from another build and mixed it in. It assumed that the same version always means the same files. The maintainers found that wasn't true for 7 of the 24 package versions they checked (&lt;a href="https://github.com/orgs/native-federation/discussions/176" rel="noopener noreferrer"&gt;4.8.0 announcement&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Mixed copies caused real bugs. In one example, &lt;code&gt;toSignal()&lt;/code&gt; failed with &lt;code&gt;NG0203&lt;/code&gt; because a borrowed &lt;code&gt;rxjs-interop&lt;/code&gt; file talked to a different Angular runtime than the rest of the component.&lt;/p&gt;

&lt;p&gt;Here's what 4.7.0 produced when the host's build had only &lt;code&gt;@angular/core&lt;/code&gt;, and &lt;code&gt;mfe2&lt;/code&gt; (20.1.5) also used &lt;code&gt;rxjs-interop&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"imports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@angular/core"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:4200/angular-core-AAA.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@angular/core/rxjs-interop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:4202/rxjs-interop-GGH.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rxjs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:4200/rxjs-BBB.js"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The page's Angular is now the host's 20.1.4 core plus &lt;code&gt;mfe2&lt;/code&gt;'s 20.1.5 &lt;code&gt;rxjs-interop&lt;/code&gt;: one copy, built from two different builds.&lt;/p&gt;

&lt;p&gt;The v3 Classic Runtime could mix builds too. In my test, a remote on the exact same version as the host used the host's &lt;code&gt;@angular/core&lt;/code&gt; together with its own &lt;code&gt;rxjs-interop&lt;/code&gt;, inside its own scope.&lt;/p&gt;

&lt;h3&gt;
  
  
  In 4.8.0: one build serves each package
&lt;/h3&gt;

&lt;p&gt;Version 4.8.0 replaces borrowing with a simple rule: &lt;strong&gt;every package is served from one build.&lt;/strong&gt; In each share scope, the Orchestrator picks one complete copy of the package, called the &lt;strong&gt;serving build&lt;/strong&gt; (&lt;a href="https://github.com/native-federation/orchestrator/blob/v4.8.0/docs/version-resolver.md#one-build-per-package" rel="noopener noreferrer"&gt;one build per package&lt;/a&gt;):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;If the host built the package,&lt;/strong&gt; the host's copy is chosen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If the host didn't,&lt;/strong&gt; it picks the remote's copy that the most remotes can use, meaning the fewest extra downloads.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then each remote is checked. If the serving build has every part the remote needs, the remote uses it. If not, the remote runs &lt;strong&gt;the whole package from its own build&lt;/strong&gt;, in its own scope. Nothing is borrowed anymore.&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%2Fog1om1m7qjpsqzym41gy.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%2Fog1om1m7qjpsqzym41gy.png" alt="How the Orchestrator decides for each remote. If the winning version doesn't fit the remote's requiredVersion, strictVersion true means the remote uses its own copy and strictVersion false means it uses the winner anyway, which is risky. If it does fit, the serving build is checked: if it includes every part the remote ships, the remote shares it. If not, in 4.8.0 the remote runs the whole package from its own build, and before 4.8.0 the missing parts were borrowed from another build, creating a mixed copy." width="800" height="553"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same files, run on 4.8.0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"imports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@angular/core"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:4200/angular-core-AAA.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rxjs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:4200/rxjs-BBB.js"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scopes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"http://localhost:4202/"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"@angular/core"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:4202/angular-core-GGG.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"@angular/core/rxjs-interop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:4202/rxjs-interop-GGH.js"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;mfe2&lt;/code&gt; uses its own complete Angular 20.1.5, while RxJS is still shared. That's two copies of Angular, but each one is complete and consistent.&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%2Fconblyrmr5bockau3rv8.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%2Fconblyrmr5bockau3rv8.png" alt="The host's build is missing rxjs-interop, which mfe2 uses. Before 4.8.0: one mixed copy with core 20.1.4 from the host's build and rxjs-interop 20.1.5 from mfe2's build, causing bugs like NG0203. In 4.8.0: two complete copies, the host's Angular with core 20.1.4 and mfe2's Angular with core and rxjs-interop 20.1.5, safe but heavier. In 4.8.0 with keepAll on the host: one complete copy for everyone, with core and rxjs-interop 20.1.4 from the host's build." width="800" height="299"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The trade-off
&lt;/h3&gt;

&lt;p&gt;4.8.0 is safer, but it can load more. A second copy of Angular can't render its components inside the host's component tree. The maintainers measured about 0.4% more downloads on average, and import maps can grow up to twice as large when many remotes fall back to their own builds (&lt;a href="https://github.com/orgs/native-federation/discussions/176" rel="noopener noreferrer"&gt;4.8.0 announcement&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The good news: with the right build settings, you get the best of both, meaning &lt;strong&gt;one complete copy&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do now
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Make the host's Angular complete with &lt;code&gt;keepAll&lt;/code&gt;.&lt;/strong&gt; It tells the build to include every part of a package, even unused ones, so the host's copy covers every remote. The config &lt;code&gt;ng add&lt;/code&gt; generates already does this for &lt;code&gt;@angular/core&lt;/code&gt;. From 4.8.0, &lt;code&gt;.patch()&lt;/code&gt; accepts wildcards, so you can cover all Angular packages at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fromPackageJson&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;strictVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;package&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="nf"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/*&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;includeSecondaries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;keepAll&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cost is small. The browser only downloads a file when some code imports it. A real example from the docs: Angular's &lt;code&gt;provideAnimationsAsync()&lt;/code&gt; loads part of &lt;code&gt;@angular/animations&lt;/code&gt; with a dynamic import that the default build doesn't detect, so without &lt;code&gt;keepAll&lt;/code&gt; the host's animations can silently never run (&lt;a href="https://github.com/native-federation/orchestrator/blob/v4.8.0/docs/version-resolver.md#when-the-hosts-build-lacks-an-entrypoint" rel="noopener noreferrer"&gt;version resolver docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Turn on &lt;code&gt;denseExternals&lt;/code&gt;, starting with the host.&lt;/strong&gt; With &lt;code&gt;features: { denseExternals: true }&lt;/code&gt;, each package is written into &lt;code&gt;remoteEntry.json&lt;/code&gt; as &lt;strong&gt;one entry&lt;/strong&gt; with one version and one range, instead of one entry per part. The Orchestrator then makes one clean decision per package. You can switch one app at a time. The maintainers expect v5 to make this the default, around the end of Q1 2027.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Keep &lt;code&gt;build: 'package'&lt;/code&gt;,&lt;/strong&gt; which &lt;code&gt;ng add&lt;/code&gt; already sets. It bundles each package on its own, so all its parts come from one bundling run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Use &lt;code&gt;convertFlatSharedInfo&lt;/code&gt; for remotes you can't rebuild yet.&lt;/strong&gt; This host option (&lt;code&gt;initFederation(manifest, { feature: { convertFlatSharedInfo: true } })&lt;/code&gt;) reads older &lt;code&gt;remoteEntry.json&lt;/code&gt; files as if they were dense. It can't add parts a build left out, though. Only rebuilding with &lt;code&gt;keepAll&lt;/code&gt; fixes that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Keep remote ranges compatible with the host.&lt;/strong&gt; If a remote's &lt;code&gt;package.json&lt;/code&gt; says &lt;code&gt;^20.1.5&lt;/code&gt; while the host is on 20.1.4, that remote can't use the host's Angular. I ran this case on 4.8.0:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With &lt;code&gt;strictVersion: true&lt;/code&gt;, &lt;code&gt;mfe2&lt;/code&gt; got its own Angular in its own scope.&lt;/li&gt;
&lt;li&gt;With &lt;code&gt;strictVersion: false&lt;/code&gt;, &lt;code&gt;mfe2&lt;/code&gt; simply used the host's 20.1.4, a version its range says it doesn't support. No error stopped it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For teams coming from v3:&lt;/strong&gt; your remotes don't need rebuilding to switch. The settings v3 ignored are already in their &lt;code&gt;remoteEntry.json&lt;/code&gt; files, and they start working as soon as the host runs the Orchestrator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A note for large monorepos:&lt;/strong&gt; one team with about 980 shared monorepo libraries reported that 4.8.0 loaded 27 of them twice, without any console errors, so they stayed on 4.7.0 for now. The cause is how library names are read. The maintainer has proposed an opt-in fix before v5 (&lt;a href="https://github.com/orgs/native-federation/discussions/176" rel="noopener noreferrer"&gt;discussion&lt;/a&gt;). If you share many internal libraries through &lt;code&gt;sharedMappings&lt;/code&gt;, test 4.8.0 carefully before upgrading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remotes added later, and caching
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Remotes added later&lt;/strong&gt; use &lt;code&gt;initRemoteEntry()&lt;/code&gt;, returned by &lt;code&gt;initFederation()&lt;/code&gt;. It follows the same rules as startup and only ever adds to the import map. You saw it with the &lt;code&gt;plugin&lt;/code&gt; remote above. In v3, the only option was a one-off &lt;code&gt;loadRemoteModule({ remoteEntry })&lt;/code&gt; call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caching&lt;/strong&gt; has a surprising default. The Orchestrator keeps its caches &lt;strong&gt;in memory&lt;/strong&gt;, so they don't survive a page reload. To keep them across reloads, choose a storage option (&lt;a href="https://native-federation.com/docs/v4/orchestrator/configuration/" rel="noopener noreferrer"&gt;configuration&lt;/a&gt;):&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;sessionStorageEntry&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;@softarc/native-federation-orchestrator/options&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;initFederation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;federation.manifest.json&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;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sessionStorageEntry&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sessionStorageEntry&lt;/code&gt; lasts for the browser tab's session, and &lt;code&gt;localStorageEntry&lt;/code&gt; survives closing the browser. v3 had no caching between page loads at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch out when you turn this on:&lt;/strong&gt; a remote redeployed &lt;strong&gt;at the same URL&lt;/strong&gt; keeps its old cached &lt;code&gt;remoteEntry.json&lt;/code&gt;, even with a &lt;code&gt;cacheTag&lt;/code&gt;. Either deploy each build at its own URL, for example a versioned path like &lt;code&gt;/mfe1/1.4.2/remoteEntry.json&lt;/code&gt;, or turn on the &lt;code&gt;overrideCachedRemotesIfURLMatches&lt;/code&gt; profile option (&lt;a href="https://github.com/native-federation/orchestrator/blob/v4.8.0/docs/config.md" rel="noopener noreferrer"&gt;configuration&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs to know
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The host always wins.&lt;/strong&gt; A remote can't bring a newer compatible version to the page while the host has the package.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strict mode is off by default, and it's all or nothing.&lt;/strong&gt; Out of the box, a version mismatch never throws an error. You can turn on strict mode, but it then applies to every shared library at once, not just to Angular.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are policy decisions, and policy is what Part 6 is about: share scopes, strict mode, and how to keep many independently deployed apps from drifting apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Series roadmap
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://dev.to/sakthicodes22/the-mental-model-revisited-why-native-federation-exists-and-what-changed-in-v4-1klg"&gt;&lt;strong&gt;The Mental Model, Revisited&lt;/strong&gt;&lt;/a&gt;: why Native Federation exists and what changed in v4&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/sakthicodes22/anatomy-of-the-v4-package-graph-core-adapters-runtime-and-orchestrator-36b8"&gt;&lt;strong&gt;Anatomy of the v4 Package Graph&lt;/strong&gt;&lt;/a&gt;: Core, Adapters, Runtime and Orchestrator as separate layers&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/sakthicodes22/build-time-end-to-end-from-federationconfig-to-remoteentryjson-1980"&gt;&lt;strong&gt;Build-Time, End to End&lt;/strong&gt;&lt;/a&gt;: from &lt;code&gt;federation.config&lt;/code&gt; to &lt;code&gt;remoteEntry.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/sakthicodes22/the-classic-v3-runtime-what-it-got-right-and-where-it-hit-its-limit-1l23"&gt;&lt;strong&gt;The Classic (v3) Runtime&lt;/strong&gt;&lt;/a&gt;: what it got right, and where it hit its limit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Orchestrator&lt;/strong&gt;: how v4 decides what loads, and what changed in 4.8.0 &lt;em&gt;(this article)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version Drift and Resolution Strategy&lt;/strong&gt;: keeping many independently deployed apps in sync&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v3 vs. v4&lt;/strong&gt;: a detailed comparison and a practical migration plan&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reference Architecture&lt;/strong&gt;: running Native Federation v4 in a regulated enterprise&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Official documentation&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/orchestrator/" rel="noopener noreferrer"&gt;Orchestrator overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/orchestrator/getting-started/" rel="noopener noreferrer"&gt;Orchestrator: getting started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/orchestrator/configuration/" rel="noopener noreferrer"&gt;Orchestrator: configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/orchestrator/version-resolver/" rel="noopener noreferrer"&gt;Orchestrator: version resolver&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/native-federation/orchestrator/blob/v4.8.0/docs/version-resolver.md" rel="noopener noreferrer"&gt;Version resolver docs in the repository (4.8.0), including "One build per package"&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://github.com/native-federation/orchestrator" rel="noopener noreferrer"&gt;Orchestrator repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/native-federation/angular-adapter/blob/main/src/index.ts" rel="noopener noreferrer"&gt;Angular adapter's &lt;code&gt;initFederation()&lt;/code&gt; wrapper&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;From the maintainers&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://github.com/orgs/native-federation/discussions/176" rel="noopener noreferrer"&gt;Orchestrator 4.8.0: one build per package (announcement)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Auke van Oostenbrugge, &lt;a href="https://medium.com/@auke997/part-ii-composing-your-own-native-federation-orchestrator-7fac2d0ea62e" rel="noopener noreferrer"&gt;Part II: Composing your own native-federation orchestrator&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Auke van Oostenbrugge, &lt;a href="https://medium.com/@auke997/part-iii-improving-cache-utilization-in-native-federation-1e4b696f3426" rel="noopener noreferrer"&gt;Part III: Improving Cache Utilization in Native Federation&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Next in this series: **Version Drift and Resolution Strategy&lt;/em&gt;&lt;em&gt;, keeping many independently deployed apps in sync.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>microfrontends</category>
      <category>nativefederation</category>
      <category>angular</category>
      <category>architecture</category>
    </item>
    <item>
      <title>The Classic (v3) Runtime - What It Got Right, and Where It Hit Its Limit</title>
      <dc:creator>Sakthikumaran Navakumar</dc:creator>
      <pubDate>Tue, 13 Oct 2026 02:36:50 +0000</pubDate>
      <link>https://dev.to/sakthicodes22/the-classic-v3-runtime-what-it-got-right-and-where-it-hit-its-limit-1l23</link>
      <guid>https://dev.to/sakthicodes22/the-classic-v3-runtime-what-it-got-right-and-where-it-hit-its-limit-1l23</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 4 of an 8-part series on Native Federation v4 for enterprise architecture teams. New to the series? Start with &lt;a href="https://dev.to/sakthicodes22/the-mental-model-revisited-why-native-federation-exists-and-what-changed-in-v4-1klg"&gt;Part 1: The Mental Model, Revisited&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The Classic Runtime is the browser runtime that shipped with Native Federation v3. Its package is &lt;code&gt;@softarc/native-federation-runtime&lt;/code&gt;. It also had a short v4 line, up to version 4.1.2, before it was retired in favor of the Orchestrator (&lt;a href="https://native-federation.com/docs/v4/runtime/" rel="noopener noreferrer"&gt;Classic Runtime docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Why look at a retired runtime? Because a lot of production apps still run on it, and you can't plan a move away from something you don't understand. Its behavior also explains many bugs teams have quietly learned to live with.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/sakthicodes22/build-time-end-to-end-from-federationconfig-to-remoteentryjson-1980"&gt;Part 3&lt;/a&gt;, we saw what the build writes into &lt;code&gt;remoteEntry.json&lt;/code&gt;. This article shows what the Classic Runtime does with that file in the browser, and what it ignores. Every claim links to the official docs or to the runtime's source code from its last release.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works, in four steps
&lt;/h2&gt;

&lt;p&gt;In a v3 app, &lt;code&gt;main.ts&lt;/code&gt; calls &lt;code&gt;initFederation()&lt;/code&gt; before Angular starts. Then the runtime does four things (&lt;a href="https://github.com/angular-architects/module-federation-plugin/blob/21.2.6/libs/native-federation-runtime/src/lib/init-federation.ts" rel="noopener noreferrer"&gt;init-federation.ts&lt;/a&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%2F4swf68qy9hxszh7tsk8w.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%2F4swf68qy9hxszh7tsk8w.png" alt="The initFederation flow: load the manifest, load the host's remoteEntry.json and put its shared libraries in the global imports, load every remote's remoteEntry.json in parallel with exposed modules going into global imports and shared libraries into a scope for that remote's URL, then add one importmap-shim script to the page." width="800" height="1520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Load the manifest,&lt;/strong&gt; the list of remote names and URLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load the host's own &lt;code&gt;remoteEntry.json&lt;/code&gt;.&lt;/strong&gt; The host's shared libraries go into the import map's global &lt;code&gt;imports&lt;/code&gt;, so the whole page can use them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load every remote's &lt;code&gt;remoteEntry.json&lt;/code&gt;, in parallel.&lt;/strong&gt; Each remote's exposed modules go into the global &lt;code&gt;imports&lt;/code&gt;. Each remote's shared libraries go into a &lt;strong&gt;scope&lt;/strong&gt;: a part of the import map that only applies to files loaded from that remote's URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add the import map to the page&lt;/strong&gt; as a &lt;code&gt;&amp;lt;script type="importmap-shim"&amp;gt;&lt;/code&gt; tag.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why &lt;code&gt;importmap-shim&lt;/code&gt;? A browser only accepts one normal import map per page, and the runtime may need to add more later. So it uses the &lt;a href="https://github.com/guybedford/es-module-shims" rel="noopener noreferrer"&gt;es-module-shims&lt;/a&gt; library, which accepts several maps and merges them (&lt;a href="https://native-federation.com/docs/v3/runtime/import-map/" rel="noopener noreferrer"&gt;v3 import map docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;After that, &lt;code&gt;loadRemoteModule('mfe1', './Component')&lt;/code&gt; finds the right file and loads it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What versions can it resolve? Only exact matches
&lt;/h2&gt;

&lt;p&gt;When the host and a remote both use a library, do they share one copy? The Classic Runtime decides with one simple rule. It keeps a list of the libraries it has already loaded, keyed by name &lt;strong&gt;and exact version&lt;/strong&gt; (&lt;a href="https://github.com/angular-architects/module-federation-plugin/blob/21.2.6/libs/native-federation-runtime/src/lib/model/externals.ts" rel="noopener noreferrer"&gt;externals.ts&lt;/a&gt;):&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;function&lt;/span&gt; &lt;span class="nf"&gt;getExternalKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SharedInfo&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;packageName&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="nx"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&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;/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%2F0rcntofhm5zmodv4o12e.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%2F0rcntofhm5zmodv4o12e.png" alt="For each shared library in a remote, the runtime builds a key like @angular/core@20.1.5. If that exact key is already registered, the remote reuses the existing file. If not, the remote's own copy is downloaded. singleton, strictVersion and requiredVersion are not checked." width="800" height="658"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Same exact key already there?&lt;/strong&gt; The remote reuses that file. The library is shared.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything else, even one patch version apart?&lt;/strong&gt; The remote downloads its own copy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The host always registers first. The &lt;a href="https://native-federation.com/docs/v3/runtime/" rel="noopener noreferrer"&gt;v3 runtime docs&lt;/a&gt; put it plainly: it's "an exact match, not a semver range." In other words, &lt;strong&gt;the Classic Runtime resolves no version ranges at all.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Seeing it in the files
&lt;/h2&gt;

&lt;p&gt;Let's follow one example from start to finish. A host and two remotes, each owned by a different team, share &lt;code&gt;@angular/core&lt;/code&gt; and &lt;code&gt;rxjs&lt;/code&gt; using the default config from Part 3: &lt;code&gt;singleton: true&lt;/code&gt;, &lt;code&gt;strictVersion: true&lt;/code&gt; and &lt;code&gt;requiredVersion: 'auto'&lt;/code&gt;. File names are shortened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: what each team declared in &lt;code&gt;package.json&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;shell/package.json&lt;/code&gt; (the host):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"@angular/core"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^20.1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rxjs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^7.8.0"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;mfe1/package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"@angular/core"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^20.1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rxjs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^7.8.0"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;mfe2/package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"@angular/core"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^20.1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rxjs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^7.8.0"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three are identical, so the teams look aligned. But &lt;code&gt;^20.1.0&lt;/code&gt; is a range. What actually gets installed depends on when each team last ran &lt;code&gt;npm install&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;App&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;@angular/core&lt;/code&gt; installed&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;rxjs&lt;/code&gt; installed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;shell (host)&lt;/td&gt;
&lt;td&gt;20.1.4&lt;/td&gt;
&lt;td&gt;7.8.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mfe1&lt;/td&gt;
&lt;td&gt;20.1.4&lt;/td&gt;
&lt;td&gt;7.8.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mfe2&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;20.1.5&lt;/strong&gt; (installed after 20.1.5 came out)&lt;/td&gt;
&lt;td&gt;7.8.2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Step 2: what each build wrote into &lt;code&gt;remoteEntry.json&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The build copies the &lt;code&gt;package.json&lt;/code&gt; range into &lt;code&gt;requiredVersion&lt;/code&gt; and records the installed version as &lt;code&gt;version&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The host, served from &lt;code&gt;http://localhost:4200/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shell"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exposes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shared"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"packageName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@angular/core"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"outFileName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"angular-core-AAA.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"20.1.4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"requiredVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^20.1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"singleton"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"strictVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"packageName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rxjs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"outFileName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rxjs-BBB.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7.8.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"requiredVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^7.8.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"singleton"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"strictVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;mfe1&lt;/code&gt;, served from &lt;code&gt;http://localhost:4201/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mfe1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exposes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./Component"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"outFileName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Component-CCC.js"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shared"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"packageName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@angular/core"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"outFileName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"angular-core-DDD.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"20.1.4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"requiredVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^20.1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"singleton"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"strictVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"packageName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rxjs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"outFileName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rxjs-EEE.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7.8.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"requiredVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^7.8.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"singleton"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"strictVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;mfe2&lt;/code&gt;, served from &lt;code&gt;http://localhost:4202/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mfe2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exposes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./Component"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"outFileName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Component-FFF.js"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shared"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"packageName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@angular/core"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"outFileName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"angular-core-GGG.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"20.1.5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"requiredVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^20.1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"singleton"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"strictVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"packageName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rxjs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"outFileName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rxjs-HHH.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7.8.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"requiredVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^7.8.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"singleton"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"strictVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;requiredVersion&lt;/code&gt; is the same everywhere. Only &lt;code&gt;version&lt;/code&gt; differs, and only for &lt;code&gt;mfe2&lt;/code&gt;'s Angular.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: the import map the runtime adds to the page&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I ran the Classic Runtime's own &lt;code&gt;initFederation()&lt;/code&gt; code against these three files. This is exactly what it produced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"imports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@angular/core"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./angular-core-AAA.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rxjs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./rxjs-BBB.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mfe1/Component"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:4201/Component-CCC.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mfe2/Component"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:4202/Component-FFF.js"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scopes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"http://localhost:4201/"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"@angular/core"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./angular-core-AAA.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"rxjs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./rxjs-BBB.js"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"http://localhost:4202/"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"@angular/core"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:4202/angular-core-GGG.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"rxjs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./rxjs-BBB.js"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How it all connects&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;imports&lt;/code&gt;&lt;/strong&gt; holds the host's libraries and every remote's exposed modules. &lt;code&gt;mfe1/Component&lt;/code&gt; is the name &lt;code&gt;loadRemoteModule('mfe1', './Component')&lt;/code&gt; looks up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;mfe1&lt;/code&gt;'s scope points at the host's files.&lt;/strong&gt; Both its versions matched exactly, so it shares Angular and RxJS. Its own copies are never downloaded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;mfe2&lt;/code&gt;'s scope is mixed.&lt;/strong&gt; RxJS 7.8.2 matched, so it's shared. Angular 20.1.5 didn't match 20.1.4, so &lt;code&gt;mfe2&lt;/code&gt; loads &lt;strong&gt;its own copy of Angular&lt;/strong&gt;, even though &lt;code&gt;^20.1.0&lt;/code&gt; allows both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;singleton&lt;/code&gt; and &lt;code&gt;strictVersion&lt;/code&gt; left no trace,&lt;/strong&gt; even though every file set them to &lt;code&gt;true&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can check this in your own app. In DevTools, find the &lt;code&gt;importmap-shim&lt;/code&gt; script in the page's &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; and compare its scopes with the &lt;code&gt;remoteEntry.json&lt;/code&gt; files in the network tab.&lt;/p&gt;

&lt;h3&gt;
  
  
  Your lockfile decides, not your package.json
&lt;/h3&gt;

&lt;p&gt;Look at what happened to &lt;code&gt;mfe2&lt;/code&gt;. The team changed no code and no config. They simply installed later, and production got a second copy of Angular. The same happens when a CI pipeline or a dependency bot updates a lockfile. Under the Classic Runtime, &lt;strong&gt;the installed version decides what gets shared.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Scale this up and it gets worse. With one host and six remotes spread over a few patch versions, a page can easily end up with four copies of Angular:&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%2F03hdaqkgpov3j7ylsc94.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%2F03hdaqkgpov3j7ylsc94.png" alt="One page, one host, six remotes, four copies of Angular. Only the host and mfe1, both on 20.1.4, share a copy. mfe2, mfe5 and mfe6 on 20.1.5 share a second copy, mfe3 on 20.2.0 has a third, and mfe4 on 19.2.0 has a fourth." width="799" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Remotes the host doesn't know up front
&lt;/h2&gt;

&lt;p&gt;Both setups from Part 3 work the same way here. Remote URLs written in &lt;code&gt;main.ts&lt;/code&gt;, or a &lt;code&gt;federation.manifest.json&lt;/code&gt; fetched at startup: either way, the list is known before the app starts, and the four steps run as described.&lt;/p&gt;

&lt;p&gt;For a remote found later, such as a plugin a user turns on, the only option is to pass its &lt;code&gt;remoteEntry.json&lt;/code&gt; URL to &lt;code&gt;loadRemoteModule()&lt;/code&gt;:&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;await&lt;/span&gt; &lt;span class="nf"&gt;loadRemoteModule&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;remoteEntry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:4203/remoteEntry.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;exposedModule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./Widget&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;I tested this with a &lt;code&gt;plugin&lt;/code&gt; remote whose lockfile installed Angular 20.1.6. The runtime fetched its &lt;code&gt;remoteEntry.json&lt;/code&gt;, applied the same exact-match rule, and added a second import map (&lt;a href="https://github.com/angular-architects/module-federation-plugin/blob/21.2.6/libs/native-federation-runtime/src/lib/load-remote-module.ts" rel="noopener noreferrer"&gt;load-remote-module.ts&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"imports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"plugin/Widget"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:4203/Widget-JJJ.js"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scopes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"http://localhost:4203/"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"@angular/core"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:4203/angular-core-KKK.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"rxjs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./rxjs-BBB.js"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same rule, same result: RxJS is shared, Angular is not. The first map is never changed. Two limits are worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's a one-off, tied to loading a module.&lt;/strong&gt; You can't register a remote in advance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The short form &lt;code&gt;loadRemoteModule('plugin', './Widget')&lt;/code&gt; fails&lt;/strong&gt; until the remote has been registered this way.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The docs list this as "No dynamic init" (&lt;a href="https://native-federation.com/docs/v4/runtime/" rel="noopener noreferrer"&gt;Classic Runtime docs&lt;/a&gt;). The Orchestrator adds &lt;code&gt;initRemoteEntry()&lt;/code&gt; for it, covered in Part 5.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your federation.config settings don't apply here
&lt;/h2&gt;

&lt;p&gt;When building the import map, the Classic Runtime reads only three fields of each shared library: &lt;code&gt;packageName&lt;/code&gt;, &lt;code&gt;version&lt;/code&gt; and &lt;code&gt;outFileName&lt;/code&gt;. You can see this in &lt;a href="https://github.com/angular-architects/module-federation-plugin/blob/21.2.6/libs/native-federation-runtime/src/lib/init-federation.ts" rel="noopener noreferrer"&gt;init-federation.ts&lt;/a&gt;. I searched every release I could find, from 16.3 to the last one, and the other settings appear only in type definitions and in &lt;a href="https://github.com/angular-architects/module-federation-plugin/blob/21.2.6/libs/native-federation-runtime/src/lib/get-shared.ts" rel="noopener noreferrer"&gt;get-shared.ts&lt;/a&gt;, a helper for working alongside webpack.&lt;/p&gt;

&lt;p&gt;So under the Classic Runtime:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;singleton: true&lt;/code&gt; doesn't stop a second copy of Angular.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;strictVersion: true&lt;/code&gt; doesn't raise an error.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;requiredVersion: '^20.1.0'&lt;/code&gt; doesn't let 20.1.5 share with 20.1.4.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've seen &lt;code&gt;strictVersion&lt;/code&gt; errors before, they came from somewhere else. webpack Module Federation throws "Unsatisfied version … of shared singleton module …" when &lt;code&gt;strictVersion&lt;/code&gt; is on. The Orchestrator can throw an &lt;code&gt;NFError&lt;/code&gt; in strict mode.&lt;/p&gt;

&lt;p&gt;But &lt;strong&gt;ignored doesn't mean wasted.&lt;/strong&gt; The build still writes these settings into &lt;code&gt;remoteEntry.json&lt;/code&gt;, and the Orchestrator acts on them. Switch runtimes, and they start working without rebuilding a single remote.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why duplicate copies hurt
&lt;/h2&gt;

&lt;p&gt;A second copy of a small utility just costs download time. For anything that holds state, it causes real bugs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Angular&lt;/strong&gt; expects to be alone on the page. Two copies break dependency injection, often with errors like &lt;code&gt;NG0203&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stateful libraries,&lt;/strong&gt; like a store or an auth client, end up with two separate instances. The host logs the user in, and the remote never finds out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These bugs are hard to trace, because each app works fine on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it got right
&lt;/h2&gt;

&lt;p&gt;To be fair, the Classic Runtime got a lot right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's built on web standards,&lt;/strong&gt; ES modules and import maps, which v4 kept.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's simple and predictable.&lt;/strong&gt; One rule, easy to follow once you know it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scopes protect the host.&lt;/strong&gt; An incompatible remote gets its own copy instead of replacing the host's.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It has useful extras:&lt;/strong&gt; a &lt;code&gt;fallback&lt;/code&gt; option in &lt;code&gt;loadRemoteModule()&lt;/code&gt;, a &lt;code&gt;cacheTag&lt;/code&gt; option for cache busting, and automatic reloads during development.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it hit its limit
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The organizational cost.&lt;/strong&gt; To share Angular, every team must ship the exact same patch version. Upgrading from 20.1.4 to 20.1.5 is no longer one team's decision. It has to be planned and released together across the host and every remote. That's lockstep releasing, which works against the main promise of micro frontends: teams that deploy independently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The operational gaps,&lt;/strong&gt; as listed in the &lt;a href="https://native-federation.com/docs/v4/runtime/" rel="noopener noreferrer"&gt;Classic Runtime docs&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No caching between page loads.&lt;/strong&gt; Every &lt;code&gt;remoteEntry.json&lt;/code&gt; is fetched again on every load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent failures.&lt;/strong&gt; A remote that fails at startup is only logged to the console, and the app starts without it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No share scopes&lt;/strong&gt; to keep one team's libraries separate from another's.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Living with it until you migrate
&lt;/h2&gt;

&lt;p&gt;If you're still on the Classic Runtime:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Find your duplicates&lt;/strong&gt; by checking the &lt;code&gt;importmap-shim&lt;/code&gt; scripts in DevTools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin exact versions&lt;/strong&gt; of Angular and other stateful libraries, and update them in every repository together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send browser console errors to your monitoring,&lt;/strong&gt; so a remote that fails at startup raises an alert.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then plan the move. The Orchestrator reads the same &lt;code&gt;remoteEntry.json&lt;/code&gt; files, so remotes don't need to change. Part 7 covers the migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Part 5 answers
&lt;/h2&gt;

&lt;p&gt;The Classic Runtime leaves three open questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How do you share a library across a &lt;strong&gt;range&lt;/strong&gt; of versions?&lt;/li&gt;
&lt;li&gt;How do you make &lt;code&gt;singleton&lt;/code&gt; and &lt;code&gt;strictVersion&lt;/code&gt; actually do something?&lt;/li&gt;
&lt;li&gt;How do you avoid fetching everything again on every page load?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Orchestrator was built to answer all three. Part 5 opens it up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Series roadmap
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://dev.to/sakthicodes22/the-mental-model-revisited-why-native-federation-exists-and-what-changed-in-v4-1klg"&gt;&lt;strong&gt;The Mental Model, Revisited&lt;/strong&gt;&lt;/a&gt;: why Native Federation exists and what changed in v4&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/sakthicodes22/anatomy-of-the-v4-package-graph-core-adapters-runtime-and-orchestrator-36b8"&gt;&lt;strong&gt;Anatomy of the v4 Package Graph&lt;/strong&gt;&lt;/a&gt;: Core, Adapters, Runtime and Orchestrator as separate layers&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/sakthicodes22/build-time-end-to-end-from-federationconfig-to-remoteentryjson-1980"&gt;&lt;strong&gt;Build-Time, End to End&lt;/strong&gt;&lt;/a&gt;: from &lt;code&gt;federation.config&lt;/code&gt; to &lt;code&gt;remoteEntry.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Classic (v3) Runtime&lt;/strong&gt;: what it got right, and where it hit its limit &lt;em&gt;(this article)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Orchestrator&lt;/strong&gt;: how version resolution and caching work, in depth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version Drift and Resolution Strategy&lt;/strong&gt;: keeping many independently deployed apps in sync&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v3 vs. v4&lt;/strong&gt;: a detailed comparison and a practical migration plan&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reference Architecture&lt;/strong&gt;: running Native Federation v4 in a regulated enterprise&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Official documentation&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/runtime/" rel="noopener noreferrer"&gt;Classic Runtime (legacy), v4 docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v3/runtime/" rel="noopener noreferrer"&gt;Runtime overview, v3 docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v3/runtime/import-map/" rel="noopener noreferrer"&gt;The import map, v3 docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v3/runtime/init-federation/" rel="noopener noreferrer"&gt;initFederation, v3 docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/orchestrator/" rel="noopener noreferrer"&gt;Orchestrator overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/migration/" rel="noopener noreferrer"&gt;Migrating to v4&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Articles by the maintainers&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Auke van Oostenbrugge, &lt;a href="https://medium.com/@auke997/tweaking-native-federation-part-ii-sharing-externals-a4e1d3711230" rel="noopener noreferrer"&gt;Tweaking native-federation, Part II: Sharing externals&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Next in this series: **The Orchestrator&lt;/em&gt;&lt;em&gt;, how version resolution and caching work, in depth.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>microfrontends</category>
      <category>nativefederation</category>
      <category>angular</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Build-Time, End to End - From federation.config to remoteEntry.json</title>
      <dc:creator>Sakthikumaran Navakumar</dc:creator>
      <pubDate>Tue, 06 Oct 2026 02:00:49 +0000</pubDate>
      <link>https://dev.to/sakthicodes22/build-time-end-to-end-from-federationconfig-to-remoteentryjson-1980</link>
      <guid>https://dev.to/sakthicodes22/build-time-end-to-end-from-federationconfig-to-remoteentryjson-1980</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 3 of an 8-part series on Native Federation v4 for enterprise architecture teams. New to the series? Start with &lt;a href="https://dev.to/sakthicodes22/the-mental-model-revisited-why-native-federation-exists-and-what-changed-in-v4-1klg"&gt;Part 1: The Mental Model, Revisited&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In Part 2, we mapped the four layers of Native Federation v4 and saw that one file, &lt;code&gt;remoteEntry.json&lt;/code&gt;, connects the build to the browser. This article zooms in on the build side and answers three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What do you write in &lt;code&gt;federation.config.mjs&lt;/code&gt;, and what does each setting mean?&lt;/li&gt;
&lt;li&gt;What happens, step by step, when you run the build, and what files come out?&lt;/li&gt;
&lt;li&gt;How does a host find remotes it has never seen when it was built?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything here is based on the official docs and the source code of &lt;a href="https://github.com/native-federation/native-federation-core" rel="noopener noreferrer"&gt;native-federation-core&lt;/a&gt; and the &lt;a href="https://github.com/native-federation/angular-adapter" rel="noopener noreferrer"&gt;Angular adapter&lt;/a&gt;, and links point to both so you can check the details yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few terms first
&lt;/h2&gt;

&lt;p&gt;Part 2 explained host, remote, shared dependency, bundler, import map, semver and singleton. This article adds four more:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;External:&lt;/strong&gt; a library that is deliberately left &lt;em&gt;out&lt;/em&gt; of your app's bundle, so it can be loaded separately and shared.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exposed module:&lt;/strong&gt; a piece of your app, such as a component or a set of routes, that other apps are allowed to load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entry point:&lt;/strong&gt; the file a build or a code scan starts from. In Angular, that's usually &lt;code&gt;src/main.ts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manifest:&lt;/strong&gt; a list of remote names and the URLs of their &lt;code&gt;remoteEntry.json&lt;/code&gt; files.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Normal build vs. federated build
&lt;/h2&gt;

&lt;p&gt;In a normal Angular build, your code and every library you use are bundled together. If three apps all use Angular, users download Angular three times.&lt;/p&gt;

&lt;p&gt;A federated build splits things up. Your own code is built without the shared libraries inside it. Each shared library gets its own file, so the browser can download it once and reuse it. And the build writes two small description files, &lt;code&gt;remoteEntry.json&lt;/code&gt; and &lt;code&gt;importmap.json&lt;/code&gt;, so the browser knows what's available.&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%2F6dsdbjgi7u7idpcyvfm9.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%2F6dsdbjgi7u7idpcyvfm9.png" alt="A normal build produces one main.js with everything inside. A federated build takes your code and federation.config.mjs and produces main.js with your code only, one file per shared library, exposed modules for remotes, and remoteEntry.json plus importmap.json." width="800" height="790"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The input: federation.config.mjs
&lt;/h2&gt;

&lt;p&gt;Every federated project has a &lt;code&gt;federation.config.mjs&lt;/code&gt; next to it. Here is the one the Angular adapter's &lt;code&gt;ng add&lt;/code&gt; schematic generates for a remote today, taken from its &lt;a href="https://github.com/native-federation/angular-adapter/blob/main/src/schematics/init/files/federation.config.mjs__tmpl__" rel="noopener noreferrer"&gt;template in the source code&lt;/a&gt; (comments shortened):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&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;withNativeFederation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fromPackageJson&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;@angular-architects/native-federation/config&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="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;withNativeFederation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mfe1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="na"&gt;exposes&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;./Component&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;./src/app/app.component.ts&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;shared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fromPackageJson&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;strictVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;package&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="c1"&gt;// Share all of @angular/core to prevent version mismatches&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&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;includeSecondaries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;keepAll&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;

  &lt;span class="na"&gt;skip&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;rxjs/ajax&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;rxjs/fetch&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;rxjs/testing&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;rxjs/webSocket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;

  &lt;span class="na"&gt;features&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;denseChunking&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="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;Let's go through it piece by piece.&lt;/p&gt;

&lt;h3&gt;
  
  
  Choosing what to share
&lt;/h3&gt;

&lt;p&gt;There are three ways to build the &lt;code&gt;shared&lt;/code&gt; list, all defined in &lt;a href="https://github.com/native-federation/native-federation-core/blob/main/src/lib/config/share-utils.ts" rel="noopener noreferrer"&gt;share-utils.ts&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;fromPackageJson()&lt;/code&gt;&lt;/strong&gt; is the recommended way. It reads the dependencies in your &lt;code&gt;package.json&lt;/code&gt; and shares all of them with the settings you pass in. Exceptions are handled with &lt;code&gt;.skip()&lt;/code&gt;, &lt;code&gt;.override()&lt;/code&gt; and &lt;code&gt;.patch()&lt;/code&gt;. The example above uses &lt;code&gt;.patch()&lt;/code&gt; to change the settings of just &lt;code&gt;@angular/core&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;shareAll()&lt;/code&gt;&lt;/strong&gt; is the older version of the same idea. It still works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;share()&lt;/code&gt;&lt;/strong&gt; is for picking libraries by hand, one by one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you leave &lt;code&gt;shared&lt;/code&gt; out entirely, Native Federation behaves as if you had written &lt;code&gt;fromPackageJson({ singleton: true, strictVersion: true, requiredVersion: 'auto' })&lt;/code&gt; (&lt;a href="https://native-federation.com/docs/v4/core/configuration/" rel="noopener noreferrer"&gt;Core configuration&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  The settings on each library
&lt;/h3&gt;

&lt;p&gt;Each shared library carries four settings that matter later, when the browser decides which version to load:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;singleton&lt;/code&gt;:&lt;/strong&gt; only one copy of this library may exist on the page. Frameworks like Angular need this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;strictVersion&lt;/code&gt;:&lt;/strong&gt; if another app needs a version that isn't compatible, don't just use the shared one anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;requiredVersion&lt;/code&gt;:&lt;/strong&gt; the range of versions this app accepts. &lt;code&gt;'auto'&lt;/code&gt; means "use the range from &lt;code&gt;package.json&lt;/code&gt;," for example &lt;code&gt;^20.1.0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;version&lt;/code&gt;:&lt;/strong&gt; the exact version this app was built with. By default it's read from the installed package in &lt;code&gt;node_modules&lt;/code&gt;, not from &lt;code&gt;package.json&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's how that plays out. Say your &lt;code&gt;package.json&lt;/code&gt; lists &lt;code&gt;"@angular/core": "^20.1.0"&lt;/code&gt; and &lt;code&gt;npm install&lt;/code&gt; gave you 20.1.4. The build records that this app &lt;em&gt;has&lt;/em&gt; 20.1.4 and &lt;em&gt;accepts&lt;/em&gt; anything from &lt;code&gt;^20.1.0&lt;/code&gt;. The browser later uses both numbers to find one version every app can agree on. Part 5 covers that decision in detail.&lt;/p&gt;

&lt;p&gt;Notice that the defaults are strict. Unless you change them, &lt;strong&gt;every library is shared as a strict singleton&lt;/strong&gt;. That's safe, but it's worth knowing before you see your first version warning.&lt;/p&gt;

&lt;h3&gt;
  
  
  skip: leaving a library out of sharing
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;skip&lt;/code&gt; takes package names, regular expressions or small functions. The important detail: skipping a library &lt;strong&gt;does not remove it&lt;/strong&gt;. It still gets installed and still works, but it's built into your app's own bundle instead of being shared. The template above skips parts of RxJS that most apps never use at runtime.&lt;/p&gt;

&lt;p&gt;Some packages are skipped automatically, through a &lt;a href="https://github.com/native-federation/native-federation-core/blob/main/src/lib/config/default-skip-list.ts" rel="noopener noreferrer"&gt;default skip list&lt;/a&gt;: Native Federation's own packages, &lt;code&gt;es-module-shims&lt;/code&gt; and all &lt;code&gt;@types/*&lt;/code&gt; packages.&lt;/p&gt;

&lt;h3&gt;
  
  
  includeSecondaries and keepAll: the sub-paths
&lt;/h3&gt;

&lt;p&gt;Many libraries have sub-paths, called secondary entry points, like &lt;code&gt;@angular/common/http&lt;/code&gt; or &lt;code&gt;@angular/core/rxjs-interop&lt;/code&gt;. &lt;code&gt;includeSecondaries&lt;/code&gt; controls whether those are shared too.&lt;/p&gt;

&lt;p&gt;Setting &lt;code&gt;includeSecondaries: { keepAll: true }&lt;/code&gt; goes one step further. It tells the build to keep &lt;em&gt;every&lt;/em&gt; sub-path of a library as long as the library itself is used, even sub-paths your app never imports. The next section explains why the template does this for &lt;code&gt;@angular/core&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the build decides what's "used"
&lt;/h2&gt;

&lt;p&gt;Sharing everything in &lt;code&gt;package.json&lt;/code&gt; would be wasteful. Most projects list libraries they barely touch. So Native Federation has a feature called &lt;strong&gt;&lt;code&gt;ignoreUnusedDeps&lt;/code&gt;&lt;/strong&gt;, which is on by default (&lt;a href="https://github.com/native-federation/native-federation-core/blob/main/src/lib/config/with-native-federation.ts" rel="noopener noreferrer"&gt;with-native-federation.ts&lt;/a&gt;). It scans your code, finds which shared libraries are actually imported, and drops the rest from the shared list.&lt;/p&gt;

&lt;p&gt;The interesting question is &lt;em&gt;where the scan starts&lt;/em&gt;. The answer depends on whether the project is a host or a remote, and it's easy to get wrong.&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%2F8nv9g5qdcsjwefsi83ey.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%2F8nv9g5qdcsjwefsi83ey.png" alt="Flow showing how ignoreUnusedDeps decides what is used. If the project exposes modules, only the exposed files are scanned. If not, the scan starts at main.ts, follows import('./bootstrap'), then the app config, routes and lazy-loaded components. Then libraries imported by those libraries are added, sub-paths are kept or dropped depending on keepAll, and everything else is removed." width="800" height="2022"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If the project exposes modules (a remote), the scan starts from the exposed files only.&lt;/strong&gt; It does not look at &lt;code&gt;main.ts&lt;/code&gt; or &lt;code&gt;bootstrap.ts&lt;/code&gt; at all. You can see this in &lt;a href="https://github.com/native-federation/native-federation-core/blob/main/src/lib/config/get-used-dependencies.ts" rel="noopener noreferrer"&gt;get-used-dependencies.ts&lt;/a&gt;: it takes the files listed in &lt;code&gt;exposes&lt;/code&gt;, and only uses the fallback entry points when that list is empty. The Angular builder's &lt;a href="https://github.com/native-federation/angular-adapter/blob/main/src/builders/build/schema.json" rel="noopener noreferrer"&gt;&lt;code&gt;entryPoints&lt;/code&gt; option&lt;/a&gt; confirms this. Its description says exposed modules "always take precedence," so you can't use it to override or narrow them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If the project exposes nothing (a typical host), the scan starts at &lt;code&gt;src/main.ts&lt;/code&gt;.&lt;/strong&gt; In a federated Angular app, &lt;code&gt;main.ts&lt;/code&gt; is tiny. It starts federation, then loads the real app with a dynamic import:&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="nf"&gt;initFederation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;federation.manifest.json&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;hostRemoteEntry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./remoteEntry.json&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="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&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;_&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./bootstrap&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="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scanner uses TypeScript's import detection, which sees dynamic &lt;code&gt;import()&lt;/code&gt; calls as well as normal imports. So it follows &lt;code&gt;import('./bootstrap')&lt;/code&gt; into &lt;code&gt;bootstrap.ts&lt;/code&gt;, then into your app config, your routes, and the components those routes lazy-load. Anything reachable from there counts as used.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then the scan adds what your libraries need.&lt;/strong&gt; If you use &lt;code&gt;@angular/router&lt;/code&gt;, the build also counts the packages &lt;code&gt;@angular/router&lt;/code&gt; itself imports, such as &lt;code&gt;@angular/common&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finally, sub-paths are checked.&lt;/strong&gt; Without &lt;code&gt;keepAll&lt;/code&gt;, a sub-path like &lt;code&gt;@angular/core/rxjs-interop&lt;/code&gt; is kept only if something imports it exactly. With &lt;code&gt;keepAll&lt;/code&gt;, it's kept whenever its parent package is used (&lt;a href="https://github.com/native-federation/native-federation-core/blob/main/src/lib/config/remove-unused-deps.ts" rel="noopener noreferrer"&gt;remove-unused-deps.ts&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;That last rule is why the template sets &lt;code&gt;keepAll&lt;/code&gt; on &lt;code&gt;@angular/core&lt;/code&gt;. Imagine two remotes on different Angular patch versions, where only one of them imports &lt;code&gt;@angular/core/rxjs-interop&lt;/code&gt;. The other remote drops that sub-path from its build. At runtime, the page can end up loading &lt;code&gt;@angular/core&lt;/code&gt; from one version and &lt;code&gt;rxjs-interop&lt;/code&gt; from another, splitting Angular across versions. The &lt;a href="https://native-federation.com/docs/v4/core/configuration/" rel="noopener noreferrer"&gt;Core configuration docs&lt;/a&gt; warn about exactly this, and &lt;code&gt;keepAll&lt;/code&gt; on framework libraries is the fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this means for remotes:&lt;/strong&gt; a library used only by a remote's own startup code, in &lt;code&gt;main.ts&lt;/code&gt; or &lt;code&gt;bootstrap.ts&lt;/code&gt;, isn't shared by that remote. It still works, because it's bundled into the remote instead. But if you expected the host and the remote to share it, check the remote's &lt;code&gt;remoteEntry.json&lt;/code&gt; rather than assuming.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when you build
&lt;/h2&gt;

&lt;p&gt;With the config understood, here's what actually runs when you type &lt;code&gt;ng build&lt;/code&gt;. The order comes from the Angular adapter's &lt;a href="https://native-federation.com/docs/v4/angular-adapter/builder/" rel="noopener noreferrer"&gt;builder documentation&lt;/a&gt; and its &lt;a href="https://github.com/native-federation/angular-adapter/blob/main/src/builders/build/builder.ts" rel="noopener noreferrer"&gt;source&lt;/a&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%2F1sq9346snb0q4bh3v4bk.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%2F1sq9346snb0q4bh3v4bk.png" alt="The build pipeline: 1. read federation.config.mjs and work out the shared list, 2. find which shared libraries are actually used, 3. reuse each shared library from the cache or build it as its own file, 4. build the exposed modules, 5. write remoteEntry.json and importmap.json, 6. Angular's ApplicationBuilder builds your app with shared libraries left out, producing dist/your-app/browser." width="784" height="2382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read the config.&lt;/strong&gt; &lt;code&gt;withNativeFederation()&lt;/code&gt; fills in defaults, applies the skip list and works out the shared list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find what's used.&lt;/strong&gt; The &lt;code&gt;ignoreUnusedDeps&lt;/code&gt; scan from the previous section trims the shared list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build the shared libraries.&lt;/strong&gt; Each one becomes its own file. Built libraries are cached in &lt;code&gt;node_modules/.cache/native-federation/&amp;lt;project&amp;gt;&lt;/code&gt;, so the next build can reuse them. The cache refreshes itself when installed versions or sharing settings change (&lt;a href="https://native-federation.com/docs/v4/core/caching/" rel="noopener noreferrer"&gt;Core caching&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build the exposed modules.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write &lt;code&gt;remoteEntry.json&lt;/code&gt; and &lt;code&gt;importmap.json&lt;/code&gt;.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hand off to Angular.&lt;/strong&gt; The adapter is a thin wrapper around Angular's own &lt;code&gt;ApplicationBuilder&lt;/code&gt;. Angular builds your app as usual, with the shared libraries marked as externals so they stay out of your bundle.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two practical notes. If the Angular build fails, &lt;strong&gt;no federation files are written at all&lt;/strong&gt;, so a failing build can never ship a half-updated &lt;code&gt;remoteEntry.json&lt;/code&gt;. And if a build ever looks stale, deleting &lt;code&gt;node_modules/.cache/native-federation&lt;/code&gt; forces a clean rebuild of the shared libraries.&lt;/p&gt;

&lt;h2&gt;
  
  
  The output: remoteEntry.json and importmap.json
&lt;/h2&gt;

&lt;h3&gt;
  
  
  remoteEntry.json: what this app offers and needs
&lt;/h3&gt;

&lt;p&gt;This is the file other apps read. Its shape is defined in &lt;a href="https://github.com/native-federation/native-federation-core/blob/main/src/lib/domain/core/federation-info.contract.ts" rel="noopener noreferrer"&gt;federation-info.contract.ts&lt;/a&gt; and written by &lt;a href="https://github.com/native-federation/native-federation-core/blob/main/src/lib/core/output/write-federation-info.ts" rel="noopener noreferrer"&gt;write-federation-info.ts&lt;/a&gt;. A simplified example (real file names include content hashes and will differ):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mfe1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exposes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./Component"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"outFileName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Component-HASH.js"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shared"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"packageName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@angular/core"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"outFileName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"angular-core-HASH.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"20.1.4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"requiredVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^20.1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"singleton"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"strictVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every setting from the config shows up here. &lt;code&gt;version&lt;/code&gt; is what this app was built with, and &lt;code&gt;requiredVersion&lt;/code&gt; is what it accepts. &lt;code&gt;singleton&lt;/code&gt; and &lt;code&gt;strictVersion&lt;/code&gt; tell the browser how strict to be. The &lt;code&gt;$version: "v4"&lt;/code&gt; marker tells runtimes which manifest format they're reading. With &lt;code&gt;denseChunking&lt;/code&gt; turned on, there's also a &lt;code&gt;chunks&lt;/code&gt; section listing the split code files.&lt;/p&gt;

&lt;h3&gt;
  
  
  importmap.json: this app's own view
&lt;/h3&gt;

&lt;p&gt;The build also writes an &lt;code&gt;importmap.json&lt;/code&gt; (&lt;a href="https://github.com/native-federation/native-federation-core/blob/main/src/lib/core/output/write-import-map.ts" rel="noopener noreferrer"&gt;write-import-map.ts&lt;/a&gt;). It maps each shared library to the file that holds it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"imports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@angular/core"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"angular-core-HASH.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rxjs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rxjs-HASH.js"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://native-federation.com/docs/v4/mental-model/" rel="noopener noreferrer"&gt;mental model docs&lt;/a&gt; describe it as "this project's view of where its externals live." The key word is &lt;em&gt;this project's&lt;/em&gt;. It only knows about one app. The import map that actually runs in the browser is built later, by the runtime, from the &lt;code&gt;remoteEntry.json&lt;/code&gt; files of the host and every remote together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Static and dynamic remotes
&lt;/h2&gt;

&lt;p&gt;This is the part that surprises people coming from webpack Module Federation: &lt;strong&gt;a Native Federation host never looks at its remotes during the build.&lt;/strong&gt; It doesn't fetch them, check them, or link against them. The only question is &lt;em&gt;where the host finds the remote URLs at runtime&lt;/em&gt;, and the &lt;code&gt;ng add&lt;/code&gt; schematic gives you two choices through &lt;code&gt;--type&lt;/code&gt; (&lt;a href="https://native-federation.com/docs/v4/angular-adapter/getting-started/" rel="noopener noreferrer"&gt;Angular adapter getting started&lt;/a&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%2F27fayxsswkist1cuxhh6.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%2F27fayxsswkist1cuxhh6.png" alt="Where the host finds remote URLs. At build time the host build never fetches or checks remotes, and each remote build publishes its own remoteEntry.json. With type host, URLs are written in main.ts and changing them means rebuilding the host. With dynamic-host, URLs live in federation.manifest.json and changing them means editing one file. Remotes can also be added later at runtime with initRemoteEntry, for example plugins. In the browser, the runtime fetches each remoteEntry.json and builds the final import map." width="799" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Static remotes (&lt;code&gt;--type host&lt;/code&gt;).&lt;/strong&gt; The remote URLs are written straight into &lt;code&gt;main.ts&lt;/code&gt; as an object. The &lt;a href="https://github.com/native-federation/angular-adapter/blob/main/src/schematics/init/steps/make-main-async.ts" rel="noopener noreferrer"&gt;schematic source&lt;/a&gt; generates something like this:&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="nf"&gt;initFederation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mfe1&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;http://localhost:4201/remoteEntry.json&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;hostRemoteEntry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./remoteEntry.json&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;It's simple, but the URLs are now part of your host's code. Moving a remote to a new URL, or deploying the same host to test and production, means rebuilding the host.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dynamic remotes (&lt;code&gt;--type dynamic-host&lt;/code&gt;, the recommended default).&lt;/strong&gt; The URLs live in a &lt;code&gt;federation.manifest.json&lt;/code&gt; in your &lt;code&gt;public/&lt;/code&gt; folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mfe1"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:4201/remoteEntry.json"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;main.ts&lt;/code&gt; just passes the file name, &lt;code&gt;initFederation('federation.manifest.json', ...)&lt;/code&gt;, and the manifest is fetched when the app starts. The same host build can run in every environment. Only the manifest changes, and it can even be generated by your deployment pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remotes discovered later.&lt;/strong&gt; Sometimes nobody knows the full list of remotes up front, as in a plugin system where customers enable features. The Orchestrator handles this with &lt;code&gt;initRemoteEntry()&lt;/code&gt;, which adds a remote after the app has started (&lt;a href="https://native-federation.com/docs/v4/orchestrator/getting-started/" rel="noopener noreferrer"&gt;Orchestrator getting started&lt;/a&gt;). It only ever adds to the import map and never changes decisions already made.&lt;/p&gt;

&lt;p&gt;Two more details from that generated &lt;code&gt;main.ts&lt;/code&gt; are worth noticing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The host publishes its own &lt;code&gt;remoteEntry.json&lt;/code&gt;.&lt;/strong&gt; Every generated &lt;code&gt;main.ts&lt;/code&gt; passes &lt;code&gt;hostRemoteEntry: { url: './remoteEntry.json' }&lt;/code&gt;. That's the host's own build output, and at runtime the host's versions win whenever a library appears in both the host and a remote (&lt;a href="https://native-federation.com/docs/v4/orchestrator/configuration/" rel="noopener noreferrer"&gt;Orchestrator configuration&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A remote can run on its own.&lt;/strong&gt; A remote's &lt;code&gt;main.ts&lt;/code&gt; calls &lt;code&gt;initFederation({}, ...)&lt;/code&gt; with an empty list. It acts as its own host, which is why you can open a remote in the browser and work on it alone.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where version resolution fits
&lt;/h2&gt;

&lt;p&gt;Everything in this article happens at build time, and the build never &lt;em&gt;chooses&lt;/em&gt; a version. It only &lt;strong&gt;records&lt;/strong&gt; them: which version each app has, which range it accepts, and how strict it wants to be. The choosing happens in the browser, when the runtime reads all the &lt;code&gt;remoteEntry.json&lt;/code&gt; files together. Part 4 looks at how the Classic Runtime made that choice, and why it often ended up loading two copies of Angular. Part 5 shows how the Orchestrator does it properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;federation.config.mjs&lt;/code&gt; decides &lt;em&gt;what&lt;/em&gt; is shared and &lt;em&gt;how strictly&lt;/em&gt;. By default, everything is a strict singleton.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;skip&lt;/code&gt; doesn't remove a library. It just stops it from being shared.&lt;/li&gt;
&lt;li&gt;With &lt;code&gt;ignoreUnusedDeps&lt;/code&gt; (on by default), a host is scanned from &lt;code&gt;main.ts&lt;/code&gt; through &lt;code&gt;bootstrap.ts&lt;/code&gt;, while a remote is scanned from its exposed modules only.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;includeSecondaries: { keepAll: true }&lt;/code&gt; on framework libraries, so sub-paths can't split a framework across versions.&lt;/li&gt;
&lt;li&gt;The build writes &lt;code&gt;remoteEntry.json&lt;/code&gt; (what this app offers and needs) and &lt;code&gt;importmap.json&lt;/code&gt; (this app's own view).&lt;/li&gt;
&lt;li&gt;A host never looks at remotes during the build. Prefer &lt;code&gt;dynamic-host&lt;/code&gt; so one build can serve every environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Series roadmap
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://dev.to/sakthicodes22/the-mental-model-revisited-why-native-federation-exists-and-what-changed-in-v4-1klg"&gt;&lt;strong&gt;The Mental Model, Revisited&lt;/strong&gt;&lt;/a&gt;: why Native Federation exists and what changed in v4&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anatomy of the v4 Package Graph&lt;/strong&gt;: Core, Adapters, Runtime and Orchestrator as separate layers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build-Time, End to End&lt;/strong&gt;: from &lt;code&gt;federation.config&lt;/code&gt; to &lt;code&gt;remoteEntry.json&lt;/code&gt; &lt;em&gt;(this article)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Classic Runtime&lt;/strong&gt;: what it got right, and where it hits its limit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Orchestrator&lt;/strong&gt;: how version resolution and caching work, in depth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version Drift and Resolution Strategy&lt;/strong&gt;: keeping many independently deployed apps in sync&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v3 vs. v4&lt;/strong&gt;: a detailed comparison and a practical migration plan&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reference Architecture&lt;/strong&gt;: running Native Federation v4 in a regulated enterprise&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Official documentation&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/mental-model/" rel="noopener noreferrer"&gt;Mental model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/architecture/" rel="noopener noreferrer"&gt;Architecture overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/core/getting-started/" rel="noopener noreferrer"&gt;Core: getting started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/core/configuration/" rel="noopener noreferrer"&gt;Core: configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/core/caching/" rel="noopener noreferrer"&gt;Core: caching&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/angular-adapter/getting-started/" rel="noopener noreferrer"&gt;Angular adapter: getting started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/angular-adapter/builder/" rel="noopener noreferrer"&gt;Angular adapter: builder&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/orchestrator/getting-started/" rel="noopener noreferrer"&gt;Orchestrator: getting started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/orchestrator/configuration/" rel="noopener noreferrer"&gt;Orchestrator: configuration&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://github.com/native-federation/native-federation-core" rel="noopener noreferrer"&gt;native-federation-core&lt;/a&gt;: see &lt;code&gt;src/lib/config&lt;/code&gt; for the config helpers and the unused-dependency scan, and &lt;code&gt;src/lib/core/output&lt;/code&gt; for the files the build writes&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/native-federation/angular-adapter" rel="noopener noreferrer"&gt;angular-adapter&lt;/a&gt;: see &lt;code&gt;src/builders/build&lt;/code&gt; for the builder and &lt;code&gt;src/schematics/init&lt;/code&gt; for what &lt;code&gt;ng add&lt;/code&gt; generates&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Articles by the maintainers&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Auke van Oostenbrugge, &lt;a href="https://medium.com/@auke997/tweaking-native-federation-part-ii-sharing-externals-a4e1d3711230" rel="noopener noreferrer"&gt;Tweaking native-federation, Part II: Sharing externals&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Auke van Oostenbrugge, &lt;a href="https://medium.com/@auke997/part-ii-composing-your-own-native-federation-orchestrator-7fac2d0ea62e" rel="noopener noreferrer"&gt;Part II: Composing your own native-federation orchestrator&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Next in this series: **The Classic Runtime&lt;/em&gt;&lt;em&gt;, what it got right, and where it hits its limit.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>microfrontends</category>
      <category>nativefederation</category>
      <category>angular</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Anatomy of the v4 Package Graph - Core, Adapters, Runtime, and Orchestrator</title>
      <dc:creator>Sakthikumaran Navakumar</dc:creator>
      <pubDate>Wed, 30 Sep 2026 01:31:47 +0000</pubDate>
      <link>https://dev.to/sakthicodes22/anatomy-of-the-v4-package-graph-core-adapters-runtime-and-orchestrator-36b8</link>
      <guid>https://dev.to/sakthicodes22/anatomy-of-the-v4-package-graph-core-adapters-runtime-and-orchestrator-36b8</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 2 of an 8-part series on Native Federation v4 for enterprise architecture teams. New to the series? Start with &lt;a href="https://dev.to/sakthicodes22/the-mental-model-revisited-why-native-federation-exists-and-what-changed-in-v4-1klg"&gt;Part 1: The Mental Model, Revisited&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/sakthicodes22/the-mental-model-revisited-why-native-federation-exists-and-what-changed-in-v4-1klg"&gt;Part 1&lt;/a&gt;, I discussed how Native Federation v4 is a platform, not just a plugin. The main evidence was that v4 split the project into several separate packages, each with its own version.&lt;/p&gt;

&lt;p&gt;🤖 A note on this article: I used Claude to help reformat and structure the content to make it clearer and more presentable for publication.&lt;/p&gt;

&lt;p&gt;But splitting code into more packages doesn't prove much on its own. The split only matters if each package has a clear job, and if you can upgrade or replace one package without breaking the others.&lt;/p&gt;

&lt;p&gt;This article tests that. It walks through the four layers of Native Federation v4 (Core, Adapters, the Classic Runtime and the Orchestrator), explains what each one is responsible for, and shows the one contract that connects them. The rest of the series builds on this map.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few terms first
&lt;/h2&gt;

&lt;p&gt;If you're new to micro frontends, these terms appear throughout the article:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Host:&lt;/strong&gt; the main application (often called the "shell") that loads other applications into itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remote:&lt;/strong&gt; a separately built and deployed application that the host loads at runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared dependency:&lt;/strong&gt; a library, like Angular or RxJS, that the host and remotes all use. Ideally the browser downloads it once and everyone reuses it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bundler:&lt;/strong&gt; the tool that turns your source code into files the browser can run, such as esbuild or webpack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Import map:&lt;/strong&gt; a standard browser feature. It's a small JSON block on the page that tells the browser where to find a module. For example: "when code asks for &lt;code&gt;rxjs&lt;/code&gt;, load it from this URL."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic versioning (semver):&lt;/strong&gt; the &lt;code&gt;major.minor.patch&lt;/code&gt; version format. A range like &lt;code&gt;^18.0.0&lt;/code&gt; means "any 18.x version is fine."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Singleton:&lt;/strong&gt; a library that must exist only once on the page. Angular is the classic example: two copies of Angular on one page cause errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  From one plugin to four repositories
&lt;/h2&gt;

&lt;p&gt;Native Federation used to live inside the &lt;code&gt;angular-architects/module-federation-plugin&lt;/code&gt; repository, alongside the same team's webpack Module Federation tools. For v4, the maintainers moved it into its own GitHub organization and split it into four repositories (&lt;a href="https://native-federation.com/docs/v4/v3-vs-v4/" rel="noopener noreferrer"&gt;v3 vs v4&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;native-federation-core:&lt;/strong&gt; the core logic. It doesn't depend on any framework or bundler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;esbuild-adapter:&lt;/strong&gt; connects the core to the esbuild bundler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;angular-adapter:&lt;/strong&gt; connects the core to Angular's build tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;orchestrator:&lt;/strong&gt; loads remotes in the browser.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For each one below, we'll ask the same question: what does it own, and what does it deliberately leave to the others?&lt;/p&gt;

&lt;h2&gt;
  
  
  The four layers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Core: runs at build time, and only at build time
&lt;/h3&gt;

&lt;p&gt;The core package is &lt;code&gt;@softarc/native-federation&lt;/code&gt;. It doesn't know about Angular, esbuild, or how a browser loads a remote. The official &lt;a href="https://native-federation.com/docs/v4/architecture/" rel="noopener noreferrer"&gt;architecture overview&lt;/a&gt; describes its job in one line: it "normalizes the federation config, bundles shared dependencies and exposed modules, and emits &lt;code&gt;remoteEntry.json&lt;/code&gt; + the import map."&lt;/p&gt;

&lt;p&gt;That job has three parts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. It defines the configuration.&lt;/strong&gt; Every federated project, whatever framework it uses, is described with the same functions (&lt;a href="https://native-federation.com/docs/v4/core/configuration/" rel="noopener noreferrer"&gt;Core configuration&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;withNativeFederation()&lt;/code&gt; holds the whole configuration: the app's &lt;code&gt;name&lt;/code&gt;, the modules it &lt;code&gt;exposes&lt;/code&gt; to other apps, and the libraries it &lt;code&gt;shared&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fromPackageJson()&lt;/code&gt; is the recommended way to list shared libraries. It shares everything in your &lt;code&gt;package.json&lt;/code&gt;, and lets you handle exceptions with &lt;code&gt;.skip()&lt;/code&gt;, &lt;code&gt;.override()&lt;/code&gt; and &lt;code&gt;.patch()&lt;/code&gt;. The older &lt;code&gt;shareAll()&lt;/code&gt; still works.&lt;/li&gt;
&lt;li&gt;Each shared library gets a few settings that matter later, when the browser decides which version to load: &lt;code&gt;singleton&lt;/code&gt; (only one copy allowed), &lt;code&gt;strictVersion&lt;/code&gt; (versions must match), &lt;code&gt;requiredVersion&lt;/code&gt; (the acceptable range, where &lt;code&gt;'auto'&lt;/code&gt; reads it from &lt;code&gt;package.json&lt;/code&gt;) and &lt;code&gt;includeSecondaries&lt;/code&gt; (whether to also share sub-paths like &lt;code&gt;@angular/common/http&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sharedMappings&lt;/code&gt; handles monorepos, where internal libraries are shared through TypeScript path aliases.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. It plans the build, but doesn't run the bundler.&lt;/strong&gt; Core decides &lt;em&gt;what&lt;/em&gt; to build. Your app is built without the shared libraries inside it, and each shared library is built separately, so the browser can download it once and reuse it. Core never compiles your code itself. It hands that work to an adapter (&lt;a href="https://native-federation.com/docs/v4/core/getting-started/" rel="noopener noreferrer"&gt;Core getting started&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. It writes the manifest.&lt;/strong&gt; Core's most important output is a file called &lt;code&gt;remoteEntry.json&lt;/code&gt;. Think of it as a remote's label. It lists what the remote exposes and which files contain it. It also lists every shared library, with the version it was built with, the version range it accepts, and its &lt;code&gt;singleton&lt;/code&gt; and &lt;code&gt;strictVersion&lt;/code&gt; settings.&lt;/p&gt;

&lt;p&gt;v4 adds two optional settings that keep this file small in large projects. &lt;code&gt;features.denseChunking&lt;/code&gt; groups split code files by bundle, and &lt;code&gt;features.denseExternals&lt;/code&gt; groups all the entry points of one library into a single entry (&lt;a href="https://native-federation.com/docs/v4/core/configuration/" rel="noopener noreferrer"&gt;Core configuration&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;What Core does &lt;em&gt;not&lt;/em&gt; do matters just as much. It doesn't turn TypeScript into JavaScript, and it doesn't decide which library version the browser loads. Because its job is small and clear, it can stay stable while everything around it changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adapters: connect a specific tool to Core
&lt;/h3&gt;

&lt;p&gt;Core works with any bundler in principle, but something has to connect it to a real one. That's all an adapter does. There are two kinds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bundler adapters&lt;/strong&gt; connect Core to one bundler. The esbuild adapter, &lt;code&gt;@softarc/native-federation-esbuild&lt;/code&gt;, is the reference one.&lt;/p&gt;

&lt;p&gt;This adapter also solves a common real-world problem: CommonJS. Browsers run modern JavaScript modules (ES modules, or ESM). Some popular libraries, React most famously, are still published in the older CommonJS format. The adapter includes a plugin, &lt;code&gt;@chialab/esbuild-plugin-commonjs&lt;/code&gt;, that converts them, and its default React setup turns it on for you. The docs also cover libraries that still don't convert cleanly (&lt;a href="https://native-federation.com/docs/v4/adapters/esbuild/getting-started/" rel="noopener noreferrer"&gt;esbuild adapter getting started&lt;/a&gt;). If your project uses older CommonJS libraries, this is where you'll run into them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Framework adapters&lt;/strong&gt; make Native Federation feel like a normal part of a framework's tools. The Angular adapter is the most mature. It plugs into &lt;code&gt;angular.json&lt;/code&gt; and hands the actual build to Angular's own &lt;code&gt;ApplicationBuilder&lt;/code&gt;. So you get every build improvement the Angular team ships, without a separate build process.&lt;/p&gt;

&lt;p&gt;You set it up with &lt;code&gt;ng add&lt;/code&gt;. The &lt;code&gt;--type&lt;/code&gt; option decides what you're creating (&lt;a href="https://native-federation.com/docs/v4/angular-adapter/getting-started/" rel="noopener noreferrer"&gt;Angular adapter getting started&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;remote&lt;/code&gt;: a micro frontend.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;host&lt;/code&gt;: a shell for one environment.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dynamic-host&lt;/code&gt;: a shell that reads its list of remotes at runtime. This is the recommended default.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The schematic updates &lt;code&gt;angular.json&lt;/code&gt;, moves your startup code into a &lt;code&gt;bootstrap.ts&lt;/code&gt; file, and creates a &lt;code&gt;federation.config.mjs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two details help with planning. First, the Angular adapter's version numbers follow Angular's, so keep them on the same major version. On Angular 20 and 21, v4 is published as &lt;code&gt;@angular-architects/native-federation-v4&lt;/code&gt;. From Angular 22, it's back under the original name, &lt;code&gt;@angular-architects/native-federation&lt;/code&gt; (&lt;a href="https://native-federation.com/docs/v4/angular-adapter/getting-started/" rel="noopener noreferrer"&gt;Angular adapter getting started&lt;/a&gt;). Second, an adapter only translates between a tool and Core. When the Angular adapter changes, it's usually because Angular's build tools changed, not because Core did.&lt;/p&gt;

&lt;p&gt;Adapters are the layer most teams work with every day: running the schematic, editing the config, and upgrading with each Angular release.&lt;/p&gt;

&lt;h3&gt;
  
  
  Classic Runtime: the original browser loader
&lt;/h3&gt;

&lt;p&gt;The Classic Runtime, &lt;code&gt;@softarc/native-federation-runtime&lt;/code&gt;, is what most existing projects still use in the browser. Its design was simple. In the words of the docs, it "read &lt;code&gt;remoteEntry.json&lt;/code&gt; files, merged them into one ES module import map, injected it into the DOM, and resolved &lt;code&gt;loadRemoteModule()&lt;/code&gt; calls against it" (&lt;a href="https://native-federation.com/docs/v4/runtime/" rel="noopener noreferrer"&gt;Runtime docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;In practice, you call &lt;code&gt;initFederation()&lt;/code&gt; in the host before the app starts, then call &lt;code&gt;loadRemoteModule()&lt;/code&gt; whenever you need a remote. It uses a small library called &lt;code&gt;es-module-shims&lt;/code&gt; so import maps work reliably, which is why the startup code is split into &lt;code&gt;main.ts&lt;/code&gt; and &lt;code&gt;bootstrap.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The catch is that it doesn't negotiate versions. It has no semver resolution and no caching between page loads (&lt;a href="https://native-federation.com/docs/v4/orchestrator/" rel="noopener noreferrer"&gt;Orchestrator docs&lt;/a&gt;). When two remotes want different versions of a library, the docs say they "simply each kept their own" (&lt;a href="https://native-federation.com/docs/v4/runtime/" rel="noopener noreferrer"&gt;Runtime docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;For a small utility library, that just costs extra download time. For a framework, it breaks things. Say the host is built with Angular 18.1.0 and a remote with Angular 18.1.1. The page ends up with two copies of Angular, and you get &lt;code&gt;NG0203&lt;/code&gt; errors (Angular's "injection context" error). Many teams have hit this in production.&lt;/p&gt;

&lt;p&gt;The Classic Runtime is now &lt;strong&gt;deprecated and end-of-life&lt;/strong&gt;. The last version is &lt;code&gt;4.1.2&lt;/code&gt;, and its npm page tells users to switch to the Orchestrator (&lt;a href="https://native-federation.com/docs/v4/runtime/" rel="noopener noreferrer"&gt;Runtime docs&lt;/a&gt;). Existing projects keep working, but there will be no more fixes. Part 4 looks at why its design hit a limit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Orchestrator: the new browser loader
&lt;/h3&gt;

&lt;p&gt;The Orchestrator, &lt;code&gt;@softarc/native-federation-orchestrator&lt;/code&gt;, reads the same &lt;code&gt;remoteEntry.json&lt;/code&gt; files as the Classic Runtime. The difference is that it actually decides which version of each library to load (&lt;a href="https://native-federation.com/docs/v4/orchestrator/" rel="noopener noreferrer"&gt;Orchestrator docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You pass it around instead of importing it.&lt;/strong&gt; &lt;code&gt;initFederation(manifest, options)&lt;/code&gt; gives you back an object that includes &lt;code&gt;loadRemoteModule&lt;/code&gt;, &lt;code&gt;load&lt;/code&gt; and &lt;code&gt;initRemoteEntry&lt;/code&gt;. You pass that object into your app rather than importing a global function. In Angular, the docs do this with an &lt;code&gt;InjectionToken&lt;/code&gt;, so it works through normal dependency injection. The options control logging, storage and how strict version checks are (&lt;a href="https://native-federation.com/docs/v4/orchestrator/getting-started/" rel="noopener noreferrer"&gt;Orchestrator getting started&lt;/a&gt;; &lt;a href="https://native-federation.com/docs/v4/orchestrator/configuration/" rel="noopener noreferrer"&gt;configuration&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It remembers what it has already loaded.&lt;/strong&gt; The Orchestrator keeps four caches (&lt;a href="https://native-federation.com/docs/v4/orchestrator/architecture/" rel="noopener noreferrer"&gt;architecture&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;where each remote lives and what it exposes&lt;/li&gt;
&lt;li&gt;shared libraries, grouped by package&lt;/li&gt;
&lt;li&gt;private libraries that belong to a single remote&lt;/li&gt;
&lt;li&gt;shared code chunks (new in v4)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By default these caches are kept in memory. You can save them with &lt;code&gt;sessionStorageEntry&lt;/code&gt; or &lt;code&gt;localStorageEntry&lt;/code&gt;, so the next page load doesn't start from scratch (&lt;a href="https://native-federation.com/docs/v4/orchestrator/getting-started/" rel="noopener noreferrer"&gt;getting started&lt;/a&gt;). This helps most with server-rendered sites that reload the whole page on every click.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It picks versions in three steps&lt;/strong&gt; (&lt;a href="https://native-federation.com/docs/v4/orchestrator/version-resolver/" rel="noopener noreferrer"&gt;version resolver&lt;/a&gt;):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sort.&lt;/strong&gt; Libraries not marked as singletons skip the negotiation, and each remote simply gets its own copy. Singletons go into a group called a &lt;em&gt;share scope&lt;/em&gt;. There's a global scope by default. You can create named scopes, such as &lt;code&gt;"team-a"&lt;/code&gt;, so one team's libraries never compete with another team's. There's also a special &lt;code&gt;"strict"&lt;/code&gt; scope, where every requested version is kept exactly as-is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose.&lt;/strong&gt; Within each scope, one version of each library wins. If the host declares a version, the host always wins. Otherwise, by default, the Orchestrator picks the version that causes the fewest extra downloads. You can switch that to "pick the newest version" with the &lt;code&gt;latestSharedExternal&lt;/code&gt; setting. Remotes that asked for an incompatible version are then handled in one of two ways. With &lt;code&gt;strictVersion: false&lt;/code&gt; (the default), they use the winning version anyway and a warning is logged. With &lt;code&gt;strictVersion: true&lt;/code&gt;, they download their own private copy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the import map.&lt;/strong&gt; The winning versions go into the import map's &lt;code&gt;imports&lt;/code&gt; section. Private copies go into &lt;code&gt;scopes&lt;/code&gt;, a per-remote section. Optional security hashes go into &lt;code&gt;integrity&lt;/code&gt; (&lt;a href="https://native-federation.com/docs/v4/orchestrator/architecture/" rel="noopener noreferrer"&gt;architecture&lt;/a&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If more remotes show up after the app starts, &lt;code&gt;initRemoteEntry()&lt;/code&gt; adds them. It only ever adds to the import map. It never changes a decision that was already made.&lt;/p&gt;

&lt;p&gt;Two design choices are worth knowing about early (&lt;a href="https://native-federation.com/docs/v4/orchestrator/version-resolver/" rel="noopener noreferrer"&gt;version resolver&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strictness is all or nothing.&lt;/strong&gt; You turn on &lt;code&gt;strict.strictExternalCompatibility&lt;/code&gt; once, in the options for &lt;code&gt;initFederation()&lt;/code&gt;. When it's on, every version mismatch throws an error instead of a warning. You can't be strict about Angular and relaxed about a small utility library in the same scope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The host's version always wins.&lt;/strong&gt; If a remote ships a newer, compatible patch, the page still loads the host's version.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither is a bug. They are deliberate choices, and they affect how you should set up your share scopes. Parts 5 and 6 cover this in depth.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one contract that connects everything
&lt;/h2&gt;

&lt;p&gt;Core and the adapters run at build time, before you deploy. The Classic Runtime and the Orchestrator run later, in the user's browser. The only thing passed from one side to the other is the manifest, &lt;code&gt;remoteEntry.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The architecture overview puts it this way: "the Core and Runtime speak a simple contract — &lt;code&gt;remoteEntry.json&lt;/code&gt; plus an import map. Everything else (which bundler, which framework, which runtime) is swappable" (&lt;a href="https://native-federation.com/docs/v4/architecture/" rel="noopener noreferrer"&gt;architecture overview&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;This is the most important idea in this article. Core doesn't need to know which runtime will read its output. It just writes a manifest in an agreed format. The runtimes don't need to know about each other, or about which Core version wrote a manifest. They just read that format.&lt;/p&gt;

&lt;p&gt;That's why the split into separate packages actually works. In practice, it means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can upgrade Core without every team having to change its runtime at the same time.&lt;/li&gt;
&lt;li&gt;During a migration, one app can use the Classic Runtime while another uses the Orchestrator, both reading manifests from the same Core version.&lt;/li&gt;
&lt;li&gt;The Orchestrator can load v3 and v4 remotes on the same page, because both follow the same contract (&lt;a href="https://native-federation.com/docs/v4/orchestrator/" rel="noopener noreferrer"&gt;Orchestrator docs&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Part 7's migration plan depends on exactly this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick reference: which package does what
&lt;/h2&gt;

&lt;p&gt;Keep this table handy. It's useful mid-migration, when you're looking at a lockfile and trying to work out what you're actually running.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Responsible for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@softarc/native-federation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Core&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;td&gt;Configuration, build planning, the &lt;code&gt;remoteEntry.json&lt;/code&gt; format&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@softarc/native-federation-esbuild&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bundler adapter&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;td&gt;Connecting esbuild, converting CommonJS libraries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;@angular-architects/native-federation&lt;/code&gt; (Angular 22+) / &lt;code&gt;-v4&lt;/code&gt; (Angular 20–21)&lt;/td&gt;
&lt;td&gt;Framework adapter&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;td&gt;Connecting Angular's build tools, schematics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@softarc/native-federation-runtime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runtime (classic)&lt;/td&gt;
&lt;td&gt;Deprecated, end-of-life&lt;/td&gt;
&lt;td&gt;Loading remotes with one merged import map, no version negotiation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@softarc/native-federation-orchestrator&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runtime (v4)&lt;/td&gt;
&lt;td&gt;Active, recommended&lt;/td&gt;
&lt;td&gt;Loading remotes, choosing versions, share scopes, caching&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Check your own project
&lt;/h2&gt;

&lt;p&gt;Before Part 3 looks at what happens during the build, take five minutes to check your own project. Open &lt;code&gt;package.json&lt;/code&gt; or your lockfile and look for these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Core:&lt;/strong&gt; which version you're on, and how far behind the latest release it is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adapter:&lt;/strong&gt; which adapter you use, and whether its major version matches your Angular version. On Angular 20 or 21, check whether you're on the &lt;code&gt;-v4&lt;/code&gt; package or still on v3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime:&lt;/strong&gt; whether you use the Classic Runtime or the Orchestrator. If you see &lt;code&gt;@softarc/native-federation-runtime&lt;/code&gt;, you depend on a package that no longer gets fixes. Plan the move now, not after the next incident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config file:&lt;/strong&gt; whether your config is still &lt;code&gt;federation.config.js&lt;/code&gt; using &lt;code&gt;require()&lt;/code&gt; and &lt;code&gt;module.exports&lt;/code&gt;. v4 expects &lt;code&gt;federation.config.mjs&lt;/code&gt;, written as an ES module, so the old style means your setup predates v4 (&lt;a href="https://native-federation.com/docs/v4/migration/" rel="noopener noreferrer"&gt;migration guide&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This check sounds basic, but it catches the most common problem teams hit when adopting v4. Usually the hard part isn't the concepts. It's finding out halfway through a migration that one layer is several versions behind where everyone thought it was.&lt;/p&gt;

&lt;h2&gt;
  
  
  Series roadmap
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://dev.to/sakthicodes22/the-mental-model-revisited-why-native-federation-exists-and-what-changed-in-v4-1klg"&gt;&lt;strong&gt;The Mental Model, Revisited&lt;/strong&gt;&lt;/a&gt;: why Native Federation exists and what changed in v4&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anatomy of the v4 Package Graph&lt;/strong&gt;: Core, Adapters, Runtime and Orchestrator as separate layers &lt;em&gt;(this article)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build-Time, End to End&lt;/strong&gt;: what Core and the adapters actually do to your code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Classic Runtime&lt;/strong&gt;: what it got right, and where it hits its limit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Orchestrator&lt;/strong&gt;: how version resolution and caching work, in depth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version Drift and Resolution Strategy&lt;/strong&gt;: keeping many independently deployed apps in sync&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v3 vs. v4&lt;/strong&gt;: a detailed comparison and a practical migration plan&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reference Architecture&lt;/strong&gt;: running Native Federation v4 in a regulated enterprise&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;&lt;strong&gt;Official documentation&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/architecture/" rel="noopener noreferrer"&gt;Architecture overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/core/getting-started/" rel="noopener noreferrer"&gt;Core: getting started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/core/configuration/" rel="noopener noreferrer"&gt;Core: configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/adapters/esbuild/getting-started/" rel="noopener noreferrer"&gt;esbuild adapter: getting started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/angular-adapter/getting-started/" rel="noopener noreferrer"&gt;Angular adapter: getting started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/runtime/" rel="noopener noreferrer"&gt;Classic Runtime (legacy)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/orchestrator/" rel="noopener noreferrer"&gt;Orchestrator overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/orchestrator/getting-started/" rel="noopener noreferrer"&gt;Orchestrator: getting started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/orchestrator/configuration/" rel="noopener noreferrer"&gt;Orchestrator: configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/orchestrator/architecture/" rel="noopener noreferrer"&gt;Orchestrator: architecture&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/orchestrator/version-resolver/" rel="noopener noreferrer"&gt;Orchestrator: version resolver&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/v3-vs-v4/" rel="noopener noreferrer"&gt;v3 vs v4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://native-federation.com/docs/v4/migration/" rel="noopener noreferrer"&gt;Migrating to v4&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Articles by the maintainers&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Auke van Oostenbrugge, &lt;a href="https://medium.com/@auke997/part-ii-composing-your-own-native-federation-orchestrator-7fac2d0ea62e" rel="noopener noreferrer"&gt;Part II: Composing your own native-federation orchestrator&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Auke van Oostenbrugge, &lt;a href="https://medium.com/@auke997/part-iii-improving-cache-utilization-in-native-federation-1e4b696f3426" rel="noopener noreferrer"&gt;Part III: Improving Cache Utilization in Native Federation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Auke van Oostenbrugge, &lt;a href="https://medium.com/@auke997/tweaking-native-federation-part-ii-sharing-externals-a4e1d3711230" rel="noopener noreferrer"&gt;Tweaking native-federation, Part II: Sharing externals&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Next in this series: **Build-Time, End to End&lt;/em&gt;&lt;em&gt;, what Core and the adapters actually do to your code.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>microfrontends</category>
      <category>nativefederation</category>
      <category>angular</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Native Federation - Deep Dive - Part 1/8 - The Mental Model, Why Native Federation Exists and What Changed in v4</title>
      <dc:creator>Sakthikumaran Navakumar</dc:creator>
      <pubDate>Sat, 19 Sep 2026 01:05:01 +0000</pubDate>
      <link>https://dev.to/sakthicodes22/the-mental-model-revisited-why-native-federation-exists-and-what-changed-in-v4-1klg</link>
      <guid>https://dev.to/sakthicodes22/the-mental-model-revisited-why-native-federation-exists-and-what-changed-in-v4-1klg</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 1 of an 8-part series on Native Federation - A deep dive.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;🤖 A note on this article: I used Claude to help reformat and structure the content to make it clearer and more presentable for publication.&lt;/p&gt;

&lt;p&gt;If you've shipped a production micro-frontend system on webpack Module Federation, you already know the mental model: a &lt;strong&gt;host&lt;/strong&gt; application lazy-loads code it never saw at compile time, from a &lt;strong&gt;remote&lt;/strong&gt; it doesn't own, and the two negotiate &lt;strong&gt;shared dependencies&lt;/strong&gt; at runtime so you're not shipping three copies of React down the wire. Native Federation didn't invent that model — it inherited it deliberately, because the model was never the problem. The &lt;em&gt;implementation&lt;/em&gt; was.&lt;/p&gt;

&lt;p&gt;This article isn't going to re-teach you what a remote is. It's going to explain why an entire second implementation of the same mental model was worth building, what structural debt it paid off, and what the v4 rewrite tells us about where the project — and the pattern — is heading. Everything that follows in this series (Core, Adapters, the Orchestrator, version drift, migration) is downstream of the argument in this article, so it's worth getting precise about it before we go deep on any one subsystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The compromise nobody talked about
&lt;/h2&gt;

&lt;p&gt;Webpack Module Federation is, technically, remarkable. It solved cross-application dynamic linking inside a tool that was never designed for it, wrapping every federated module in a runtime container and negotiation layer that webpack manages on your behalf. It works. Thousands of production systems prove it works.&lt;/p&gt;

&lt;p&gt;But it works &lt;em&gt;inside webpack's universe&lt;/em&gt;. Every remote, every host, every build step in that architecture is mediated by webpack's own module runtime — its own resolution logic, its own chunk-loading machinery, its own container format. That's not a criticism of the implementation; it's an acknowledgment of what problem webpack was actually solving when it introduced Module Federation: how do you retrofit dynamic, runtime remote loading onto a bundler whose entire value proposition is &lt;em&gt;static&lt;/em&gt; dependency analysis at build time? The container pattern is the necessary compromise.&lt;/p&gt;

&lt;p&gt;The compromise has a cost, and if you've operated Module Federation at scale, you've paid it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Framework lock-in at the tooling layer.&lt;/strong&gt; Your host and every remote are implicitly committed to webpack (or a webpack-compatible bundler) for the life of the federated system, because the runtime contract &lt;em&gt;is&lt;/em&gt; a webpack runtime contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A parallel module system living alongside the real one.&lt;/strong&gt; The browser has a native module system — ES Modules — that has matured enormously in recent years. Module Federation doesn't use it; it builds its own resolution and loading logic on top of webpack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration friction that compounds.&lt;/strong&gt; Moving a host off webpack — to Vite, to esbuild, to whatever ships next — means either dragging Module Federation's runtime along as a dependency of a tool you're trying to leave, or re-architecting your federation layer as part of a bundler migration. Two hard problems, coupled.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this made Module Federation the wrong choice when it was the only serious option. It made it a choice with an expiration date tied to how long webpack stayed the default.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "browser-native" actually buys you
&lt;/h2&gt;

&lt;p&gt;Native Federation's core bet is structural: instead of building a container protocol on top of a bundler's runtime, build directly on &lt;strong&gt;ECMAScript Modules and Import Maps&lt;/strong&gt; — both of which are web standards the browser already implements, with no bundler runtime mediating the load.&lt;/p&gt;

&lt;p&gt;Concretely, that means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A remote's exposed module is a genuine ES module, served as a genuine &lt;code&gt;.js&lt;/code&gt; file, importable with a genuine dynamic &lt;code&gt;import()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Shared dependency resolution happens through the browser's &lt;strong&gt;import map&lt;/strong&gt; mechanism — a JSON structure that tells the browser "when code asks for &lt;code&gt;react&lt;/code&gt;, resolve it to &lt;em&gt;this&lt;/em&gt; URL" — rather than through a bundler-authored sharing runtime.&lt;/li&gt;
&lt;li&gt;The build tool's job shrinks to what a build tool should do: produce standards-compliant output and a manifest (&lt;code&gt;remoteEntry.json&lt;/code&gt;) describing what's exposed and what's shared. It stops being responsible for &lt;em&gt;how the browser resolves modules at runtime&lt;/em&gt;, because the browser already knows how to do that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the part that's easy to undersell as "just an implementation detail," but for an architecture team it's the whole point: &lt;strong&gt;the bundler becomes replaceable&lt;/strong&gt;. Native Federation ships a reference esbuild adapter and — most maturely — an Angular adapter that hooks directly into Angular's own esbuild-based Application Builder, but the underlying contract (ESM + import maps + a JSON manifest) doesn't require any of that. Swap the build tool, keep the federation contract. That's not true of container-based Module Federation, and it's the single biggest reason to treat this as an infrastructure decision independent of your current framework and bundler choices — not just an Angular-ecosystem convenience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters more in v4 than it did in v3
&lt;/h2&gt;

&lt;p&gt;Native Federation existed well before v4, largely as a feature of the Angular CLI plugin ecosystem — genuinely useful, but organizationally still shaped like "a clever thing the Angular Architects team built for Angular users." v3 proved the ESM/import-map model worked in production. &lt;/p&gt;

&lt;p&gt;v4 is the rewrite that makes that claim credible, and the evidence isn't a changelog bullet point — it's organizational. The project moved out of the original &lt;code&gt;angular-architects/module-federation-plugin&lt;/code&gt; monorepo entirely and now lives under its own GitHub organization, split into independently versioned repositories: a framework-and-bundler-agnostic &lt;strong&gt;core&lt;/strong&gt;, a dedicated &lt;strong&gt;esbuild adapter&lt;/strong&gt;, a dedicated &lt;strong&gt;Angular adapter&lt;/strong&gt;, and a separately maintained &lt;strong&gt;orchestrator&lt;/strong&gt; for runtime consumption. &lt;/p&gt;

&lt;p&gt;That's not cosmetic restructuring. It's the difference between "a plugin with internal modules" and "a platform with published contracts between its layers." When a build tool, a framework adapter, and a runtime consumer are three separately versioned artifacts instead of three files in the same package, you get to ask a much sharper architectural question about any given piece of the system: &lt;em&gt;what does this layer promise to the layer above it, and what can I swap out without breaking that promise?&lt;/em&gt; We'll answer that question concretely for every layer over the next two articles.&lt;/p&gt;

&lt;p&gt;The other structural signal in v4 worth naming now, because it recurs throughout this series: the project explicitly frames itself as moving toward first-class support for frameworks beyond Angular, and toward runtime consumption from environments that aren't single-page applications at all — plain HTML pages, and server-rendered hosts in different ecosystems, consuming the same manifest contract. Angular remains the most mature adapter today by a wide margin, and this series will go deep on it because that's where the tooling is production-ready — but the &lt;em&gt;ceiling&lt;/em&gt; of the architecture is no longer "an Angular thing." That ceiling matters when you're making a five-year platform bet, not just a next-quarter delivery decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Series Roadmap
&lt;/h2&gt;

&lt;p&gt;This is an 8-part series, and each article builds on the ones before it. Here's the full plan, and where we are right now:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Mental Model, Revisited&lt;/strong&gt; — why Native Federation exists and what changed in v4 &lt;em&gt;(this article)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anatomy of the v4 Package Graph&lt;/strong&gt; — Core, Adapters, Runtime, and Orchestrator as independently versioned layers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build-Time, End to End&lt;/strong&gt; — the Core library and its Adapters, and what they actually do to your code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Classic Runtime&lt;/strong&gt; — what it got right, and where it hits its ceiling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Orchestrator&lt;/strong&gt; — semver-aware resolution and persistent caching, under the hood&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version Drift and Resolution Strategy&lt;/strong&gt; — the governance problem nobody's build pipeline catches&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v3 vs. v4&lt;/strong&gt; — a systematic comparison and a real migration playbook&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reference Architecture&lt;/strong&gt; — running Native Federation v4 in a regulated enterprise&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Article 2 is next: it takes the org restructuring covered above seriously and maps the actual package graph, so you know precisely which layer you're touching, and which promises it's making to the layers around it, before we go deep on any single one.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Next in this series: **Anatomy of the v4 Package Graph&lt;/em&gt;* — mapping Core, Adapters, Classic Runtime, and the Orchestrator as independently versioned layers with explicit contracts between them.*&lt;/p&gt;

</description>
      <category>microfrontends</category>
      <category>nativefederation</category>
      <category>architecture</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Angular Schematics Deep Dive — Part 1: Understanding the Architecture</title>
      <dc:creator>Sakthikumaran Navakumar</dc:creator>
      <pubDate>Mon, 09 Mar 2026 02:53:25 +0000</pubDate>
      <link>https://dev.to/sakthicodes22/angular-schematics-deep-dive-part-1-understanding-the-architecture-46i4</link>
      <guid>https://dev.to/sakthicodes22/angular-schematics-deep-dive-part-1-understanding-the-architecture-46i4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This is Part 1 of a 6-part series on Angular Schematics.&lt;/strong&gt; The complete roadmap is listed at the end of this article. Future parts cover custom generators (&lt;code&gt;ng generate&lt;/code&gt;), installation schematics (&lt;code&gt;ng add&lt;/code&gt;), migration schematics (&lt;code&gt;ng update&lt;/code&gt;), testing with Angular DevKit, and advanced patterns including Nx integration.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;Every time you run &lt;code&gt;ng generate component&lt;/code&gt;, &lt;code&gt;ng add @angular/material&lt;/code&gt;, or &lt;code&gt;ng update&lt;/code&gt;, something remarkably sophisticated is happening beneath the surface. Angular's CLI isn't simply copying template files — it is executing a structured, transactional, and fully testable code transformation pipeline. That pipeline is powered by &lt;strong&gt;Angular Schematics&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For many Angular developers, schematics remain a black box — something the framework uses internally, something Nx and Angular Material ship with, something you might glance at and walk away from. That's a missed opportunity. Understanding schematics is the difference between a developer who &lt;em&gt;uses&lt;/em&gt; Angular tooling and one who &lt;em&gt;extends&lt;/em&gt; it.&lt;/p&gt;

&lt;p&gt;In this first article, we pull back the curtain completely. We'll examine the conceptual model, the runtime architecture, the four core abstractions, the virtual file system, and the execution pipeline that every schematic runs through. By the end, you'll have a precise mental model of how schematics work — which is the essential prerequisite for building your own.&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;What Exactly Is a Schematic?&lt;/li&gt;
&lt;li&gt;How the Angular CLI Uses Schematics&lt;/li&gt;
&lt;li&gt;Architecture Overview — The Four Pillars&lt;/li&gt;
&lt;li&gt;The Schematic Execution Pipeline&lt;/li&gt;
&lt;li&gt;Exploring the Built-in Schematics Collection&lt;/li&gt;
&lt;li&gt;When to Create Custom Schematics&lt;/li&gt;
&lt;li&gt;Summary &amp;amp; What's Next&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What Exactly Is a Schematic?
&lt;/h2&gt;

&lt;p&gt;At its core, a schematic is a &lt;strong&gt;pure function that describes file system transformations&lt;/strong&gt;. It receives a representation of the file system as input and returns a description of how that file system should change — without directly touching disk until all transformations are validated and committed.&lt;/p&gt;

&lt;p&gt;That definition contains three ideas worth unpacking carefully.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. It's a pure function.&lt;/strong&gt; Schematics follow functional programming principles. A schematic doesn't mutate the world directly. It receives state, produces a new description of state, and hands that description off to a runner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. It describes transformations — it doesn't execute them.&lt;/strong&gt; Creating a file in a schematic isn't the same as calling &lt;code&gt;fs.writeFileSync()&lt;/code&gt;. You're building a description of what should happen, giving the framework an opportunity to validate, preview, and batch everything before committing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. It operates on a virtual file system.&lt;/strong&gt; The file system schematics work with is an in-memory representation. This is what makes dry-run mode, transactional rollbacks, and unit testing possible and practical.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;A useful mental model:&lt;/strong&gt; Think of a schematic the way a database thinks of a transaction. You open a transaction (the virtual Tree), describe all your changes (Rules), and then either commit them all atomically or roll back entirely. No partial states, no corrupted workspaces.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  How the Angular CLI Uses Schematics
&lt;/h2&gt;

&lt;p&gt;Angular CLI commands map directly to schematics. When you run any of the following, you are invoking a schematic from a registered collection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Invokes the 'component' schematic from @schematics/angular&lt;/span&gt;
ng generate component my-feature

&lt;span class="c"&gt;# Invokes the 'ng-add' schematic from @angular/material's package.json&lt;/span&gt;
ng add @angular/material

&lt;span class="c"&gt;# Invokes migration schematics listed in the package's migrations.json&lt;/span&gt;
ng update @angular/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This mapping is not magic — it's configuration. Every Angular library that participates in this ecosystem declares its schematics in &lt;code&gt;package.json&lt;/code&gt; through two fields:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Example Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;"schematics"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Path to collection.json for ng generate / ng add&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"./schematics/collection.json"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;"ng-update"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Points to migrations.json for ng update&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{"migrations": "./migrations.json"}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;collection.json&lt;/code&gt; acts as a manifest — a registry of all named schematics the package exposes, the TypeScript factory function for each, and the JSON schema that defines the schematic's options. The CLI reads this manifest, resolves the factory, and hands it to the DevKit runtime.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Overview — The Four Pillars
&lt;/h2&gt;

&lt;p&gt;The Angular DevKit (&lt;code&gt;@angular-devkit/schematics&lt;/code&gt;) is built around four foundational abstractions. Understanding each one precisely is the prerequisite for writing effective schematics.&lt;/p&gt;




&lt;h3&gt;
  
  
  1. Tree — The Virtual File System
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;Tree&lt;/code&gt; is the representation of the workspace as the schematic sees it. It is an immutable snapshot combined with a &lt;strong&gt;staged layer of mutations&lt;/strong&gt; that haven't been applied yet. When you call &lt;code&gt;tree.create()&lt;/code&gt; or &lt;code&gt;tree.overwrite()&lt;/code&gt;, you are adding entries to that staging layer — not touching disk.&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;Tree&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;@angular-devkit/schematics&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;mySchematic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MyOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Tree&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// READ — returns buffer or null (no I/O hits disk)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;angular.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// WRITE — stages a change, does NOT write to disk&lt;/span&gt;
    &lt;span class="nx"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/my-file.ts&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;export const x = 42;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// OVERWRITE — also staged, not committed yet&lt;/span&gt;
    &lt;span class="nx"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;overwrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/app/app.module.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;updatedContent&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;tree&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// return the mutated tree&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;The Tree also exposes &lt;code&gt;tree.exists()&lt;/code&gt;, &lt;code&gt;tree.delete()&lt;/code&gt;, &lt;code&gt;tree.rename()&lt;/code&gt;, and &lt;code&gt;tree.getDir()&lt;/code&gt; for directory traversal. Everything is synchronous and backed by in-memory buffers unless you are reading a file that only exists on disk and hasn't been modified yet.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Rule — The Unit of Work
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;Rule&lt;/code&gt; is the atomic unit of transformation in schematics. Its type signature is elegantly simple:&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;// From @angular-devkit/schematics internals&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Tree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SchematicContext&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;Tree&lt;/span&gt; &lt;span class="o"&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;Tree&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;Rule&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rules are composable. The DevKit provides &lt;code&gt;chain()&lt;/code&gt; to compose multiple rules sequentially, &lt;code&gt;branchAndMerge()&lt;/code&gt; to run rules on a branched copy of the Tree, and &lt;code&gt;mergeWith()&lt;/code&gt; to merge a generated file source into the existing Tree. This composability is what makes schematics architecturally powerful — complex operations are built from simple, independently testable pieces.&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;Rule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mergeWith&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&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;@angular-devkit/schematics&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;myFeature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Rule&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;chain&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nf"&gt;addFilesFromTemplates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;// Rule 1&lt;/span&gt;
    &lt;span class="nf"&gt;updateAngularJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;      &lt;span class="c1"&gt;// Rule 2&lt;/span&gt;
    &lt;span class="nf"&gt;addImportToAppModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;// Rule 3&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;h3&gt;
  
  
  3. Source — Generating a New Tree
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;Source&lt;/code&gt; is a factory that produces an entirely new, empty Tree from some origin — typically a directory of template files. The most commonly used Source is &lt;code&gt;url()&lt;/code&gt;, which reads template files from a local directory. Sources are merged into the main Tree using &lt;code&gt;mergeWith()&lt;/code&gt;.&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;apply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;applyTemplates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;move&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mergeWith&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;@angular-devkit/schematics&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;strings&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;@angular-devkit/core&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;templateSource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./files&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="nf"&gt;applyTemplates&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;dasherize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;strings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dasherize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;strings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="nf"&gt;move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;mergeWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;templateSource&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  4. SchematicContext — Runtime Information
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;SchematicContext&lt;/code&gt; is passed alongside the Tree into every Rule. It provides access to the logger (use this instead of &lt;code&gt;console.log&lt;/code&gt;), task scheduling (for running npm install after file generation), and the engine executing the schematic. It's the schematic's connection to the outside world.&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;SchematicContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NodePackageInstallTask&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;@angular-devkit/schematics&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;tree&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Tree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SchematicContext&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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Installing dependencies...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Schedule npm install to run AFTER all file mutations commit&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NodePackageInstallTask&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;tree&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Schematic Execution Pipeline
&lt;/h2&gt;

&lt;p&gt;With the four pillars understood, let's trace the full lifecycle of a schematic execution from CLI command to committed files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CLI Command
───────────
ng generate component dashboard

     │
     ▼
CLI resolves collection
─────────────────────────
Reads package.json → "schematics": "./schematics/collection.json"
Reads collection.json → finds "component" entry → factory path

     │
     ▼
JSON Schema Validation
──────────────────────
Merges CLI flags + prompts against schema.json
Validates required fields, applies defaults
Prompts user for missing required options

     │
     ▼
Factory Function Invoked
─────────────────────────
schematic(options) → returns root Rule

     │
     ▼
Engine Creates Tree
────────────────────
Base layer  = current workspace on disk
Staging layer = empty (no mutations yet)

     │
     ▼
Rule Chain Executes
────────────────────
Rule 1: Generate files from templates → staged
Rule 2: Update angular.json            → staged
Rule 3: Add import to app.module.ts    → staged

     │
     ▼
Post-Rule Tasks Run
────────────────────
NodePackageInstallTask (if scheduled)
RunSchematicTask (chained schematics)

     │
     ▼
Commit (unless --dry-run)
──────────────────────────
Staging layer flushed to real disk
Console output: CREATE / UPDATE / DELETE logs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Exploring the Built-in Schematics Collection
&lt;/h2&gt;

&lt;p&gt;The best way to learn schematic patterns is to read production-grade schematics. The entire &lt;code&gt;@schematics/angular&lt;/code&gt; package is open source and worth studying carefully.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install to inspect locally&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; @schematics/angular &lt;span class="nt"&gt;--save-dev&lt;/span&gt;

&lt;span class="c"&gt;# Browse on GitHub:&lt;/span&gt;
&lt;span class="c"&gt;# github.com/angular/angular-cli/tree/main/packages/schematics/angular&lt;/span&gt;

&lt;span class="c"&gt;# List all available schematics in the built-in collection&lt;/span&gt;
ng generate &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside &lt;code&gt;@schematics/angular&lt;/code&gt; you'll find the implementation for every built-in generator: &lt;code&gt;component&lt;/code&gt;, &lt;code&gt;service&lt;/code&gt;, &lt;code&gt;module&lt;/code&gt;, &lt;code&gt;guard&lt;/code&gt;, &lt;code&gt;pipe&lt;/code&gt;, &lt;code&gt;directive&lt;/code&gt;, &lt;code&gt;class&lt;/code&gt;, &lt;code&gt;enum&lt;/code&gt;, &lt;code&gt;interface&lt;/code&gt;, and &lt;code&gt;application&lt;/code&gt;. Each one follows the same pattern: a &lt;code&gt;schema.json&lt;/code&gt; defining options, an &lt;code&gt;index.ts&lt;/code&gt; factory, and a &lt;code&gt;files/&lt;/code&gt; directory of templates.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Schematic&lt;/th&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;Interesting Technique&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;component&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/component/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Conditional template rendering based on &lt;code&gt;--standalone&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;module&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/module/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Routing module generation, lazy-load wiring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;guard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/guard/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Multiple implementations based on &lt;code&gt;--implements&lt;/code&gt; flag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;application&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/application/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Complete workspace scaffolding, cross-schematic calls&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Practical Tip:&lt;/strong&gt; When learning schematics, clone the Angular CLI repo and read the &lt;code&gt;component&lt;/code&gt; schematic in its entirety. It demonstrates conditional template inclusion, AST-based module updates, path normalization, and option defaults — all the patterns you'll use in production schematics.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  When to Create Custom Schematics
&lt;/h2&gt;

&lt;p&gt;Having established the technical foundation, the natural question is: when does writing a custom schematic become the right investment?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repeated scaffolding patterns.&lt;/strong&gt; If your team frequently creates a "feature module" that always includes a component, a service, a store, and a routing module — and that setup consistently takes 15 minutes of copy-pasting — you have a schematic waiting to be written. A single &lt;code&gt;ng generate feature dashboard&lt;/code&gt; should handle it in under a second.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enforcing architectural rules.&lt;/strong&gt; Custom schematics are a powerful mechanism for ensuring generated code follows your team's conventions. Folder structures, naming patterns, required barrel files, mandatory test setups — all of these can be encoded into a schematic rather than documented in a wiki that no one reads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Library installation complexity.&lt;/strong&gt; If your internal UI library requires updating &lt;code&gt;angular.json&lt;/code&gt;, modifying global styles, adding a provider to &lt;code&gt;app.config.ts&lt;/code&gt;, and installing two peer dependencies — an &lt;code&gt;ng add&lt;/code&gt; schematic automates all of that into a single, repeatable, idempotent operation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Framework migrations.&lt;/strong&gt; When you introduce a breaking change in your shared library — a renamed method, a restructured API, a changed import path — an &lt;code&gt;ng update&lt;/code&gt; schematic can apply that migration automatically across every workspace that consumes your library. This is how Angular itself ships breaking changes responsibly.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🚦 &lt;strong&gt;The Decision Test:&lt;/strong&gt; Ask yourself: &lt;em&gt;"Would I describe this task in an onboarding doc?"&lt;/em&gt; If yes, the task is probably worth automating with a schematic. Documentation describes what humans should do manually. Schematics eliminate the need for the documentation entirely.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Summary &amp;amp; What's Next
&lt;/h2&gt;

&lt;p&gt;We've covered significant ground. Here's what to take forward:&lt;/p&gt;

&lt;p&gt;A schematic is a &lt;strong&gt;pure function that describes file system transformations&lt;/strong&gt; on a virtual, in-memory representation of the workspace. The Angular CLI maps its core commands directly to schematics declared in package manifests. The DevKit runtime is built on four core abstractions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tree&lt;/strong&gt; — virtual file system with a staging layer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rule&lt;/strong&gt; — atomic, composable unit of transformation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source&lt;/strong&gt; — template file origin that produces a new Tree&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SchematicContext&lt;/strong&gt; — runtime connection for logging and task scheduling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The virtual file system is the design decision that enables everything valuable about schematics: dry-run previews, atomic rollback, and millisecond-fast unit testing. The execution pipeline runs schema validation, factory invocation, rule chain execution, task scheduling, and a final atomic commit.&lt;/p&gt;

&lt;p&gt;Understanding this architecture deeply is not just academic. It is the precise foundation upon which every practical schematic is built. In &lt;strong&gt;Part 2&lt;/strong&gt;, we take this knowledge directly into the editor and build our first real custom generator — a feature module scaffolder that produces a complete, production-ready module structure in a single command.&lt;/p&gt;




&lt;h2&gt;
  
  
  Series Roadmap
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Part 1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Understanding Angular Schematics — Architecture &amp;amp; Core Concepts&lt;/td&gt;
&lt;td&gt;✅ You are here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Part 2&lt;/td&gt;
&lt;td&gt;Creating Custom Generators with &lt;code&gt;ng generate&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;🔜 Coming Soon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Part 3&lt;/td&gt;
&lt;td&gt;Building Installation Schematics with &lt;code&gt;ng add&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;🔜 Coming Soon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Part 4&lt;/td&gt;
&lt;td&gt;Writing Migration Schematics with &lt;code&gt;ng update&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;🔜 Coming Soon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Part 5&lt;/td&gt;
&lt;td&gt;Testing Schematics with Angular DevKit&lt;/td&gt;
&lt;td&gt;🔜 Coming Soon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Part 6&lt;/td&gt;
&lt;td&gt;Advanced Patterns, Publishing &amp;amp; Monorepo Integration&lt;/td&gt;
&lt;td&gt;🔜 Coming Soon&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Previous Blogs:
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/sakthicodes22/the-nx-cheatsheet-commands-for-daily-development-3h4k"&gt;https://dev.to/sakthicodes22/the-nx-cheatsheet-commands-for-daily-development-3h4k&lt;/a&gt;&lt;br&gt;
&lt;a href="https://dev.to/sakthicodes22/stop-the-spaghetti-enforcing-module-boundaries-in-an-nx-monorepo-2a24"&gt;https://dev.to/sakthicodes22/stop-the-spaghetti-enforcing-module-boundaries-in-an-nx-monorepo-2a24&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Found this useful? Drop a ❤️ and share it with a teammate who would be intersted.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>schematics</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Nx Cheatsheet — Commands for Daily Development</title>
      <dc:creator>Sakthikumaran Navakumar</dc:creator>
      <pubDate>Mon, 02 Mar 2026 15:17:27 +0000</pubDate>
      <link>https://dev.to/sakthicodes22/the-nx-cheatsheet-commands-for-daily-development-3h4k</link>
      <guid>https://dev.to/sakthicodes22/the-nx-cheatsheet-commands-for-daily-development-3h4k</guid>
      <description>&lt;p&gt;If you have just started working with &lt;strong&gt;Nx&lt;/strong&gt; — or joined a team that uses it — the CLI can feel overwhelming at first. There are dozens of commands, flags, and concepts to absorb all at once.&lt;/p&gt;

&lt;p&gt;This cheatsheet cuts through the noise. It covers the &lt;strong&gt;commands you will actually use every day&lt;/strong&gt;, organised by workflow, explained plainly. Bookmark it, come back to it, share it with your team. Complete List of CLI commands can be found &lt;a href="https://nx.dev/docs/reference/nx-commands" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  📋 Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;What is Nx?&lt;/li&gt;
&lt;li&gt;Creating a Workspace&lt;/li&gt;
&lt;li&gt;Adding Plugins&lt;/li&gt;
&lt;li&gt;Generating Projects and Code&lt;/li&gt;
&lt;li&gt;Running and Serving Applications&lt;/li&gt;
&lt;li&gt;Running Multiple Tasks&lt;/li&gt;
&lt;li&gt;Filtering Projects with Patterns and Tags&lt;/li&gt;
&lt;li&gt;Building Projects&lt;/li&gt;
&lt;li&gt;The Affected Commands&lt;/li&gt;
&lt;li&gt;Skipping and Managing the Cache&lt;/li&gt;
&lt;li&gt;Running Tests&lt;/li&gt;
&lt;li&gt;Linting and Formatting&lt;/li&gt;
&lt;li&gt;Visualising the Dependency Graph&lt;/li&gt;
&lt;li&gt;Workspace and Project Info&lt;/li&gt;
&lt;li&gt;The Nx Daemon&lt;/li&gt;
&lt;li&gt;Quick Reference Cheat Sheet&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What is Nx?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Nx&lt;/strong&gt; is an open-source build system and monorepo toolkit. It lets you manage all your applications and shared libraries in a single repository while giving you intelligent tooling to build, test, and serve them efficiently.&lt;/p&gt;

&lt;p&gt;Three things make Nx stand out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🧠 &lt;strong&gt;Smart task runner&lt;/strong&gt; — understands your project dependency graph and only rebuilds what has actually changed&lt;/li&gt;
&lt;li&gt;🏗️ &lt;strong&gt;Code generation&lt;/strong&gt; — scaffolds apps, libraries, and components using best-practice templates&lt;/li&gt;
&lt;li&gt;📊 &lt;strong&gt;Visualisation&lt;/strong&gt; — interactive graph to see how all your projects connect&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Creating a Workspace
&lt;/h2&gt;

&lt;p&gt;Everything starts here. A workspace is the root container for all your applications and libraries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create a new workspace interactively&lt;/span&gt;
npx create-nx-workspace@latest

&lt;span class="c"&gt;# Create with a specific framework preset&lt;/span&gt;
&lt;span class="c"&gt;# (react, angular, next, node, ts, empty)&lt;/span&gt;
npx create-nx-workspace@latest &lt;span class="nt"&gt;--preset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;react

&lt;span class="c"&gt;# Create a blank monorepo with a custom name&lt;/span&gt;
npx create-nx-workspace@latest &lt;span class="nt"&gt;--preset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;empty &lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;myorg

&lt;span class="c"&gt;# Specify your preferred package manager (npm, yarn, pnpm)&lt;/span&gt;
npx create-nx-workspace@latest &lt;span class="nt"&gt;--packageManager&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pnpm

&lt;span class="c"&gt;# Add Nx to an already-existing project or monorepo&lt;/span&gt;
nx init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. Adding Plugins
&lt;/h2&gt;

&lt;p&gt;Nx is plugin-driven. Plugins provide the framework-specific generators and executors for React, Angular, Node, and more. The &lt;code&gt;nx add&lt;/code&gt; command installs a plugin &lt;strong&gt;and&lt;/strong&gt; automatically runs its initialisation generator in one step — no manual wiring required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install the React plugin and auto-initialize it&lt;/span&gt;
nx add @nx/react

&lt;span class="c"&gt;# Install the Angular plugin and wire it up automatically&lt;/span&gt;
nx add @nx/angular
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  3. Generating Projects and Code
&lt;/h2&gt;

&lt;p&gt;Instead of manually creating folders and configuration files, let Nx do it for you. The &lt;code&gt;nx generate&lt;/code&gt; (or &lt;code&gt;nx g&lt;/code&gt;) command creates everything following best practices for your chosen framework.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generate a new application&lt;/span&gt;
nx g app &amp;lt;n&amp;gt;

&lt;span class="c"&gt;# Generate a new shared library&lt;/span&gt;
nx g lib &amp;lt;n&amp;gt;

&lt;span class="c"&gt;# Generate a component (framework-specific)&lt;/span&gt;
nx g component &amp;lt;n&amp;gt;

&lt;span class="c"&gt;# Preview what will be generated WITHOUT writing any files&lt;/span&gt;
nx g app &amp;lt;n&amp;gt; &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. Running and Serving Applications
&lt;/h2&gt;

&lt;p&gt;Nx provides a consistent interface for running your projects regardless of which framework or tooling is underneath. &lt;code&gt;nx serve myapp&lt;/code&gt; is shorthand for &lt;code&gt;nx run myapp:serve&lt;/code&gt; — both do the same thing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Start a local development server&lt;/span&gt;
nx serve &amp;lt;project&amp;gt;

&lt;span class="c"&gt;# Run any defined target on a project&lt;/span&gt;
nx run &amp;lt;project&amp;gt;:&amp;lt;target&amp;gt;

&lt;span class="c"&gt;# Run a target with a specific configuration (e.g. production, staging)&lt;/span&gt;
nx run &amp;lt;project&amp;gt;:&amp;lt;target&amp;gt;:&amp;lt;configuration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Running Multiple Tasks
&lt;/h2&gt;

&lt;p&gt;As your monorepo grows, you need to run operations across multiple projects at once. &lt;code&gt;nx run-many&lt;/code&gt; fans out work across your workspace in parallel. The default parallelism is &lt;code&gt;3&lt;/code&gt; — increase it based on your machine's capacity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run multiple targets across ALL projects in one command&lt;/span&gt;
nx run-many &lt;span class="nt"&gt;-t&lt;/span&gt; build &lt;span class="nb"&gt;test &lt;/span&gt;lint

&lt;span class="c"&gt;# Run multiple targets on specific named projects only&lt;/span&gt;
nx run-many &lt;span class="nt"&gt;-t&lt;/span&gt; build &lt;span class="nb"&gt;test &lt;/span&gt;lint &lt;span class="nt"&gt;-p&lt;/span&gt; app1 app2

&lt;span class="c"&gt;# Run tasks with a concurrency limit&lt;/span&gt;
nx run-many &lt;span class="nt"&gt;-t&lt;/span&gt; build &lt;span class="nt"&gt;--parallel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5

&lt;span class="c"&gt;# Run with the interactive Terminal UI (new in Nx v22)&lt;/span&gt;
nx run-many &lt;span class="nt"&gt;-t&lt;/span&gt; build &lt;span class="nt"&gt;--outputStyle&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tui
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6. Filtering Projects with Patterns and Tags
&lt;/h2&gt;

&lt;p&gt;Instead of listing every project name manually, use glob patterns and Nx project tags to target exactly the right set of projects. Tags are labels you assign to projects in their config — for example &lt;code&gt;scope:frontend&lt;/code&gt; or &lt;code&gt;type:ui&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run only projects tagged with scope:frontend&lt;/span&gt;
nx run-many &lt;span class="nt"&gt;-t&lt;/span&gt; build &lt;span class="nt"&gt;--projects&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'tag:scope:frontend'&lt;/span&gt;

&lt;span class="c"&gt;# Run only projects whose names end with -app&lt;/span&gt;
nx run-many &lt;span class="nt"&gt;-t&lt;/span&gt; build &lt;span class="nt"&gt;--projects&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*-app'&lt;/span&gt;

&lt;span class="c"&gt;# Mix explicit names and glob patterns&lt;/span&gt;
nx run-many &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--projects&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'app1,app2,shared-*'&lt;/span&gt;

&lt;span class="c"&gt;# Run on all projects, EXCLUDING e2e projects&lt;/span&gt;
nx run-many &lt;span class="nt"&gt;-t&lt;/span&gt; lint &lt;span class="nt"&gt;--exclude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*-e2e'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  7. Building Projects
&lt;/h2&gt;

&lt;p&gt;Building compiles your source code and produces deployment-ready artifacts. The command is the same regardless of whether you use Webpack, Vite, or esbuild underneath.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build a specific project&lt;/span&gt;
nx build &amp;lt;project&amp;gt;

&lt;span class="c"&gt;# Build using the production configuration (optimised, minified)&lt;/span&gt;
nx build &amp;lt;project&amp;gt; &lt;span class="nt"&gt;--configuration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;production

&lt;span class="c"&gt;# Build all projects in the workspace&lt;/span&gt;
nx run-many &lt;span class="nt"&gt;-t&lt;/span&gt; build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  8. The Affected Commands — Nx's Superpower
&lt;/h2&gt;

&lt;p&gt;This is the feature that makes Nx genuinely transformative. Instead of rebuilding and retesting everything on every commit, Nx analyses your dependency graph and figures out &lt;strong&gt;exactly which projects are affected by your changes&lt;/strong&gt; — and only runs tasks for those.&lt;/p&gt;

&lt;p&gt;If you change a utility library, Nx identifies every app and library that depends on it and runs the target for all of them. Everything else is skipped. On a large monorepo, this can reduce CI time significantly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build only projects affected by recent changes&lt;/span&gt;
nx affected &lt;span class="nt"&gt;-t&lt;/span&gt; build

&lt;span class="c"&gt;# Test only affected projects&lt;/span&gt;
nx affected &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;

&lt;span class="c"&gt;# Lint only affected projects&lt;/span&gt;
nx affected &lt;span class="nt"&gt;-t&lt;/span&gt; lint

&lt;span class="c"&gt;# Run multiple targets on affected projects in one command&lt;/span&gt;
nx affected &lt;span class="nt"&gt;-t&lt;/span&gt; build &lt;span class="nb"&gt;test &lt;/span&gt;lint

&lt;span class="c"&gt;# Determine affected projects relative to a specific branch/commit range&lt;/span&gt;
nx affected &lt;span class="nt"&gt;--base&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;main &lt;span class="nt"&gt;--head&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;HEAD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  9. Skipping and Managing the Cache
&lt;/h2&gt;

&lt;p&gt;Nx caches the results of every task. If nothing has changed since the last run, Nx replays the cached result in milliseconds. There are times, however, when you need to bypass the cache — debugging a flaky test, verifying a fix actually works, or clearing stale output.&lt;/p&gt;

&lt;p&gt;Refer here for &lt;a href="https://nx.dev/docs/concepts/how-caching-works" rel="noopener noreferrer"&gt;How caching works&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run a build bypassing the local cache entirely&lt;/span&gt;
nx build &amp;lt;project&amp;gt; &lt;span class="nt"&gt;--skipNxCache&lt;/span&gt;

&lt;span class="c"&gt;# Skip cache for all projects in run-many&lt;/span&gt;
nx run-many &lt;span class="nt"&gt;-t&lt;/span&gt; build &lt;span class="nt"&gt;--skipNxCache&lt;/span&gt;

&lt;span class="c"&gt;# Skip cache on all affected test runs&lt;/span&gt;
nx affected &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--skipNxCache&lt;/span&gt;

&lt;span class="c"&gt;# Clear the entire local cache AND daemon state&lt;/span&gt;
nx reset

&lt;span class="c"&gt;# Clear ONLY the task cache, leave the daemon running&lt;/span&gt;
nx reset &lt;span class="nt"&gt;--only-cache&lt;/span&gt;

&lt;span class="c"&gt;# Restart ONLY the daemon, leave the cache intact&lt;/span&gt;
nx reset &lt;span class="nt"&gt;--only-daemon&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  10. Running Tests
&lt;/h2&gt;

&lt;p&gt;Whether your projects use Jest, Vitest, Playwright, or Cypress, &lt;code&gt;nx test&lt;/code&gt; and &lt;code&gt;nx e2e&lt;/code&gt; provide a consistent interface. Combine with &lt;code&gt;affected&lt;/code&gt; and &lt;code&gt;run-many&lt;/code&gt; for full-scale coverage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run unit tests for a specific project&lt;/span&gt;
nx &lt;span class="nb"&gt;test&lt;/span&gt; &amp;lt;project&amp;gt;

&lt;span class="c"&gt;# Run end-to-end tests for a project&lt;/span&gt;
nx e2e &amp;lt;project&amp;gt;

&lt;span class="c"&gt;# Run unit tests only for affected projects&lt;/span&gt;
nx affected &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;

&lt;span class="c"&gt;# Run unit tests across ALL projects&lt;/span&gt;
nx run-many &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  11. Linting and Formatting
&lt;/h2&gt;

&lt;p&gt;Keeping code quality consistent across a monorepo is hard without tooling. Nx gives every project its own lint target and provides workspace-level format commands powered by Prettier.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Lint a specific project&lt;/span&gt;
nx lint &amp;lt;project&amp;gt;

&lt;span class="c"&gt;# Lint only the projects affected by recent changes&lt;/span&gt;
nx affected &lt;span class="nt"&gt;-t&lt;/span&gt; lint

&lt;span class="c"&gt;# Auto-format ALL files in the workspace using Prettier&lt;/span&gt;
nx format:write

&lt;span class="c"&gt;# Check formatting across the workspace WITHOUT writing (perfect for CI)&lt;/span&gt;
nx format:check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  12. Visualising the Dependency Graph
&lt;/h2&gt;

&lt;p&gt;At any point you can visualise the entire architecture of your monorepo as an interactive browser-based graph. This is invaluable for onboarding new team members, auditing architecture, and understanding the blast radius of a change.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Open the interactive dependency graph in the browser&lt;/span&gt;
nx graph

&lt;span class="c"&gt;# Show only the subgraph of a specific project and its dependencies&lt;/span&gt;
nx graph &lt;span class="nt"&gt;--focus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;project&amp;gt;

&lt;span class="c"&gt;# Print the project graph as JSON to the terminal&lt;/span&gt;
nx graph &lt;span class="nt"&gt;--print&lt;/span&gt;

&lt;span class="c"&gt;# Export the full graph to a JSON file&lt;/span&gt;
nx graph &lt;span class="nt"&gt;--file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;output.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  13. Workspace and Project Info
&lt;/h2&gt;

&lt;p&gt;As your workspace grows, you need reliable ways to explore it. The &lt;code&gt;nx show&lt;/code&gt;, &lt;code&gt;nx list&lt;/code&gt;, and &lt;code&gt;nx report&lt;/code&gt; commands answer the everyday questions: what exists, what is affected, what plugins are installed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# List ALL projects in the workspace&lt;/span&gt;
nx show projects

&lt;span class="c"&gt;# List only the projects affected by recent changes&lt;/span&gt;
&lt;span class="c"&gt;# (modern replacement for the deprecated nx affected:apps/libs)&lt;/span&gt;
nx show projects &lt;span class="nt"&gt;--affected&lt;/span&gt;

&lt;span class="c"&gt;# List all projects that have a specific target defined&lt;/span&gt;
nx show projects &lt;span class="nt"&gt;--with-target&lt;/span&gt; serve

&lt;span class="c"&gt;# Show full configuration details of a specific project&lt;/span&gt;
nx show project &amp;lt;n&amp;gt;

&lt;span class="c"&gt;# Open a rich browser view of a project's fully resolved configuration&lt;/span&gt;
nx show project &amp;lt;n&amp;gt; &lt;span class="nt"&gt;--web&lt;/span&gt;

&lt;span class="c"&gt;# List all installed Nx plugins in the workspace&lt;/span&gt;
nx list

&lt;span class="c"&gt;# List all generators and executors provided by a specific plugin&lt;/span&gt;
nx list &amp;lt;plugin&amp;gt;

&lt;span class="c"&gt;# Print all installed Nx and plugin versions (essential for debugging)&lt;/span&gt;
nx report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  14. The Nx Daemon
&lt;/h2&gt;

&lt;p&gt;The Nx Daemon is a background process that keeps your workspace's project graph in memory. Without it, every command has to re-read and re-parse your entire workspace from scratch — noticeable latency in large monorepos. In most cases it starts and stops automatically. These commands give you manual control when needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Manually start the Nx daemon&lt;/span&gt;
nx daemon &lt;span class="nt"&gt;--start&lt;/span&gt;

&lt;span class="c"&gt;# Stop the Nx daemon&lt;/span&gt;
nx daemon &lt;span class="nt"&gt;--stop&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ⚡ Quick Reference Cheat Sheet
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🏗️ Workspace Setup
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;npx create-nx-workspace@latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a new workspace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx init&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Add Nx to an existing project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx add @nx/react&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Add and initialize a plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  🔧 Generate Code
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx g app &amp;lt;n&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;New application&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx g lib &amp;lt;n&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;New shared library&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx g component &amp;lt;n&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;New component&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx g app &amp;lt;n&amp;gt; --dry-run&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Preview generation without writing files&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  ▶️ Run Tasks
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx serve &amp;lt;project&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start dev server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx build &amp;lt;project&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Build a project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx test &amp;lt;project&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run unit tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx e2e &amp;lt;project&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run e2e tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx lint &amp;lt;project&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lint a project&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  🚀 Scale Across Projects
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx run-many -t build test lint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run multiple targets across all projects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx run-many -t build --parallel=5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run with concurrency control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx run-many -t build --projects='*-app'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Target projects by glob pattern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx run-many -t build --projects='tag:scope:frontend'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Target projects by tag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx run-many -t lint --exclude='*-e2e'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Exclude projects by pattern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx affected -t build test lint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only run what is affected by your changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx affected --base=main --head=HEAD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Affected in a PR commit range&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  🗄️ Cache Management
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx build &amp;lt;project&amp;gt; --skipNxCache&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bypass cache for one task&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx reset --only-cache&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Clear task cache only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx reset --only-daemon&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Restart daemon only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx reset&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full reset — cache + daemon&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  🔍 Explore &amp;amp; Debug
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx graph&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Open interactive dependency graph&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx graph --focus=&amp;lt;project&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Focus graph on one project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx show projects --affected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List affected projects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx show project &amp;lt;n&amp;gt; --web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Inspect full project config in browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx report&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Check all installed plugin versions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx format:check&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Check formatting (great for CI)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nx format:write&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Auto-format the entire workspace&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




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

&lt;ul&gt;
&lt;li&gt;📖 &lt;a href="https://nx.dev/docs" rel="noopener noreferrer"&gt;Official Nx Documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🔧 &lt;a href="https://nx.dev/docs/reference/nx-commands" rel="noopener noreferrer"&gt;Nx CLI Reference (v22)&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Found this useful? Drop a ❤️ and share it with a teammate who is just getting started with Nx.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nx</category>
      <category>monorepo</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Stop the Spaghetti: Enforcing Module Boundaries in an Nx Monorepo</title>
      <dc:creator>Sakthikumaran Navakumar</dc:creator>
      <pubDate>Sat, 28 Feb 2026 14:44:59 +0000</pubDate>
      <link>https://dev.to/sakthicodes22/stop-the-spaghetti-enforcing-module-boundaries-in-an-nx-monorepo-2a24</link>
      <guid>https://dev.to/sakthicodes22/stop-the-spaghetti-enforcing-module-boundaries-in-an-nx-monorepo-2a24</guid>
      <description>&lt;h2&gt;
  
  
  Enforcing Module Boundaries in an Nx Monorepo
&lt;/h2&gt;

&lt;p&gt;It starts with good intentions. Your team decides to adopt a monorepo. You scaffold your workspace, create a handful of libraries, and for the first few sprints everything feels clean and purposeful. Then the deadlines hit.&lt;/p&gt;

&lt;p&gt;A developer needs a formatting utility from the payments module — so they reach in and import it directly. Another engineer needs a component from the loans feature for a quick prototype in the accounts section. Someone else, pressed for time, imports a service three layers deep from another domain's internals. Nobody reviews it too carefully. The CI pipeline is green. Ship it.&lt;/p&gt;

&lt;p&gt;Six months later, your dependency graph looks like a bowl of spaghetti. The payments domain knows about accounts. The accounts feature imports from loans. Shared utilities carry domain-specific logic. Nobody can confidently change anything without triggering a cascade of broken imports across the workspace. Your architecture diagram, proudly mounted on the team's Confluence page, bears no resemblance to what the codebase actually does.&lt;br&gt;
This is not a discipline problem. It is a tooling problem. And Nx solves it elegantly.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Module Boundaries Matter: The Architectural Case
&lt;/h2&gt;

&lt;p&gt;Before we look at the tooling, it is worth understanding why boundary enforcement is not just a nice-to-have but an architectural necessity in any sufficiently large codebase.&lt;/p&gt;
&lt;h2&gt;
  
  
  Implicit Coupling Is the Silent Killer
&lt;/h2&gt;

&lt;p&gt;In a monorepo without enforced boundaries, any library can import from any other library. Technically, there is nothing stopping a UI component from reaching directly into a data-access service, or a feature module from importing internal implementation details from a completely unrelated domain. These imports create implicit coupling — undocumented, invisible dependencies that accumulate quietly until refactoring becomes genuinely dangerous.&lt;/p&gt;
&lt;h2&gt;
  
  
  Social Enforcement Does Not Scale
&lt;/h2&gt;

&lt;p&gt;With a team of three engineers and ten libraries, you can maintain architectural discipline through code review and shared understanding. With a team of fifteen engineers, forty libraries, and three concurrent feature tracks, you cannot. The cognitive overhead of manually auditing import paths in code review is enormous, the feedback loop is slow, and violations slip through. You need the tooling to carry the architectural intent forward, independent of team size, experience level, or deadline pressure.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Architecture Diagram Should Match the Import Graph
&lt;/h2&gt;

&lt;p&gt;This is the principle that underpins everything. If your architecture diagram shows that the payments domain is isolated from the loans domain, then your import graph should reflect exactly that. Nx module boundary enforcement is the mechanism that keeps these two things in sync — automatically, continuously, and without relying on human vigilance.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Nx Mental Model: Tags and Dependency Constraints
&lt;/h2&gt;

&lt;p&gt;Nx enforces boundaries through a tag-based system. Each library in your workspace is assigned one or more tags via its project.json file, and your ESLint configuration defines rules that govern which tags are allowed to depend on which other tags.&lt;/p&gt;

&lt;p&gt;Tags typically carry two dimensions of information: scope and type.&lt;/p&gt;

&lt;p&gt;Scope answers the question: which domain or vertical does this library belong to? In a banking application, you might have scopes like scope:payments, scope:loans, scope:accounts, scope:kyc (Know Your Customer), and scope:shared.&lt;/p&gt;

&lt;p&gt;Type answers the question: what architectural layer or role does this library play? Rather than the generic feature/ui/data-access/util convention, a more expressive and flexible approach uses tags like type:app, type:lib, type:shared, and type:e2e. This maps more naturally to how libraries actually behave in large-scale production workspaces and gives you coarser, more durable rules.&lt;/p&gt;

&lt;p&gt;Here is how tags are applied in a library's project.json:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//apps/banking-portal-e&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;e/project.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"banking-portal-e2e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"projectType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"application"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"scope:banking-portal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"type:e2e"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once your libraries are tagged, the ESLint rule &lt;code&gt;@nx/enforce-module-boundaries&lt;/code&gt; becomes the enforcement engine.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Sample Project Structure
&lt;/h2&gt;

&lt;p&gt;Before diving into the ESLint configuration, here is the workspace structure we will be working with throughout this article. This represents a simplified but realistic banking platform monorepo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;banking-workspace/
├── apps/
│   ├── banking-portal/       # Customer-facing web app
│   │   └── project.json      # tags: scope:banking-portal, type:app
│   ├── banking-portal-e2e/   # E2E tests for banking-portal
│   │   └── project.json      # tags: scope:banking-portal, type:e2e
│   ├── admin-dashboard/      # Internal ops/admin app
│       └── project.json      # tags: scope:admin, type:app  
├── libs/
│   ├── payments/
│   │   ├── feature-transfer/ # tags: scope:payments, type:lib
│   │   ├── feature-transaction-history/ # tags: scope:payments, type:lib
│   │   └── data-access/      # tags: scope:payments, type:lib
│   ├── loans/
│   │   ├── feature-apply/    # tags: scope:loans, type:lib
│   │   ├── feature-repayment/# tags: scope:loans, type:lib
│   │   └── data-access/      # tags: scope:loans, type:lib
│   ├── accounts/
│   │   ├── feature-dashboard/# tags: scope:accounts, type:lib
│   │   ├── feature-settings/ # tags: scope:accounts, type:lib
│   │   └── data-access/      # tags: scope:accounts, type:lib
│   ├── kyc/
│   │   ├── feature-onboarding/# tags: scope:kyc, type:lib
│   │   └── data-access/       # tags: scope:kyc, type:lib
│   └── shared/
│       ├── ui-design-system/  # tags: scope:shared, type:shared
│       ├── ui-forms/          # tags: scope:shared, type:shared
│       ├── util-formatters/   # tags: scope:shared, type:shared
│       ├── util-validators/   # tags: scope:shared, type:shared
│       └── data-access-http/  # tags: scope:shared, type:shared
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every domain (payments, loans, accounts, kyc) is a self-contained vertical. The shared scope holds anything that is genuinely cross-cutting. Apps consume libs. Libs do not reach into other domain's libs unless explicitly permitted. E2E projects only test — they never become a source of shared logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enforcing Boundaries with the Nx ESLint Plugin
&lt;/h2&gt;

&lt;p&gt;With the project structure established, let us look at the ESLint configuration that encodes these architectural rules. All of this lives in your root .eslintrc.json.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"root"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plugins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"@nx"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"overrides"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"*.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*.tsx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*.jsx"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"@nx/enforce-module-boundaries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"enforceBuildableLibDependency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"allowCircularSelfDependency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"banTransitiveDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"depConstraints"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;

              &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"sourceTag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"type:app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"onlyDependOnLibsWithTags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"type:lib"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"type:shared"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

              &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"sourceTag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"type:lib"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"onlyDependOnLibsWithTags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"type:lib"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"type:shared"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

              &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"sourceTag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"type:shared"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"onlyDependOnLibsWithTags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"type:shared"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

              &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"sourceTag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"type:e2e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"onlyDependOnLibsWithTags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"type:shared"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

              &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"sourceTag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"scope:payments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"onlyDependOnLibsWithTags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"scope:payments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"scope:shared"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"sourceTag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"scope:loans"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"onlyDependOnLibsWithTags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"scope:loans"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"scope:shared"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"sourceTag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"scope:accounts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"onlyDependOnLibsWithTags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"scope:accounts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"scope:shared"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"sourceTag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"scope:kyc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"onlyDependOnLibsWithTags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"scope:kyc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"scope:shared"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"sourceTag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"scope:shared"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"onlyDependOnLibsWithTags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"scope:shared"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  This configuration encodes six architectural decisions simultaneously:
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Apps can consume domain libs and shared libs — but never other apps&lt;/li&gt;
&lt;li&gt;Domain libs can consume other libs within their own scope and anything in shared — but never reach across domain boundaries&lt;/li&gt;
&lt;li&gt;Shared libs are the foundation layer — they are self-contained and import nothing outside their own scope&lt;/li&gt;
&lt;li&gt;E2E projects can reference shared utilities if needed, but are otherwise isolated from application and domain logic&lt;/li&gt;
&lt;li&gt;banTransitiveDependencies ensures that if lib A depends on lib B which depends on lib C, lib A cannot import from lib C directly — it must go through lib B's public API&lt;/li&gt;
&lt;li&gt;enforceBuildableLibDependencyCheck ensures that if you enable buildable libs, your dependency declarations stay honest&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Advanced Tag Expressions: !, *, and Combining Constraints
&lt;/h2&gt;

&lt;p&gt;One of the most underused and underappreciated features of the @nx/enforce-module-boundaries rule is its support for tag expression operators. Beyond simple string matching, the rule supports negation, wildcards, and compound logic that lets you write precise, expressive constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Wildcard Operator: *
&lt;/h2&gt;

&lt;p&gt;The wildcard * matches any tag. This is useful when you want to say "this library can import from literally anything" — which you should use sparingly, but it has legitimate use cases for certain shell or orchestration libraries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sourceTag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"type:app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"onlyDependOnLibsWithTags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &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="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More practically, wildcards shine when used within a tag expression for partial matching. For example, if you want to allow a shared utility to depend on any shared library regardless of a sub-classification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;json
{
  "sourceTag": "scope:shared",
  "onlyDependOnLibsWithTags": ["scope:shared", "*:shared"]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also use ! directly within the &lt;em&gt;onlyDependOnLibsWithTags&lt;/em&gt; array to say "must have this tag and must not have that tag simultaneously":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;json{
  "sourceTag": "scope:accounts",
  "onlyDependOnLibsWithTags": ["scope:accounts", "scope:shared", "!type:e2e"]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reads as: "accounts-scoped libraries may only depend on libraries that are tagged scope:accounts or scope:shared, and in all cases, those libraries must not be tagged type:e2e." This is particularly useful when your shared scope contains a mix of test utilities and production utilities that you want to distinguish at the dependency constraint level.&lt;/p&gt;

&lt;p&gt;Violations in Action: What This Looks Like in Your IDE&lt;br&gt;
Let us see what happens when a developer violates one of these rules. This is arguably the most important section of the article because it shows the tangible developer experience of boundary enforcement.&lt;br&gt;
typescript// libs/payments/feature-transfer/src/lib/transfer.component.ts&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;// ✅ ALLOWED — payments lib importing from its own domain&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;PaymentsApiService&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;@banking/payments/data-access&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ ALLOWED — payments lib importing from shared scope&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;CurrencyFormatterPipe&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;@banking/shared/util-formatters&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;ButtonComponent&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;@banking/shared/ui-design-system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ❌ VIOLATION — crossing domain boundaries&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;LoanEligibilityService&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;@banking/loans/data-access&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ESLint Error: "A project tagged with 'scope:payments' can only depend on &lt;/span&gt;
&lt;span class="c1"&gt;// libs tagged with 'scope:payments' or 'scope:shared'"&lt;/span&gt;

&lt;span class="c1"&gt;// ❌ VIOLATION — lib importing from an app&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;AppConfig&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;@banking/banking-portal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ESLint Error: "A project tagged with 'type:lib' cannot depend on &lt;/span&gt;
&lt;span class="c1"&gt;// libs tagged with 'type:app'"&lt;/span&gt;

&lt;span class="c1"&gt;// ❌ VIOLATION — reaching into e2e project&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;MockApiInterceptor&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;@banking/banking-portal-e2e&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ESLint Error: "Imports of e2e projects are not permitted"&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;These errors surface in real time in VS Code (with the ESLint extension installed).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architectural Payoff
&lt;/h2&gt;

&lt;p&gt;Once module boundaries are in place and running in CI, the benefits compound over time in ways that go well beyond code organization.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your architecture becomes self-documenting. A new engineer joining the banking platform team can read the ESLint configuration and understand the entire domain structure and dependency rules in under five minutes. The rules tell the same story as the architecture diagram — because they are the architecture diagram, expressed as code.&lt;/li&gt;
&lt;li&gt;Code reviews become faster and more focused. Reviewers no longer need to manually audit import paths or ask questions like "should payments really know about loans?" The linter has already answered that question before the pull request was opened. Reviews can focus entirely on logic, correctness, and intent.&lt;/li&gt;
&lt;li&gt;Refactoring becomes safe. Because every library exposes only what its index.ts exports, and because boundaries prevent cross-domain imports, you can refactor a library's internals with surgical confidence. The blast radius of any change is bounded by the public API surface.&lt;/li&gt;
&lt;li&gt;Teams can work in parallel without stepping on each other. When domain boundaries are enforced, the payments team and the loans team genuinely cannot create accidental dependencies between their work. Conway's Law works in your favour: the organizational structure is mirrored by — and protected by — the module boundary rules.&lt;/li&gt;
&lt;li&gt;The architecture survives team turnover. Senior engineers who understand the original design decisions eventually leave. Without tooling, their architectural intent leaves with them. With boundary enforcement, the decisions are encoded in the linting configuration and outlive any individual contributor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The companion repository for this article, including the full workspace structure, ESLint configuration, sample library implementations, and intentional violations with their error output, is available at &lt;a href="https://github.com/sakthikumaran22/nx-module-boundaries-demo" rel="noopener noreferrer"&gt;github&lt;/a&gt;. Clone it, break the rules, and watch the linter push back.&lt;/p&gt;

&lt;p&gt;github: &lt;a href="https://github.com/sakthikumaran22/nx-module-boundaries-demo" rel="noopener noreferrer"&gt;https://github.com/sakthikumaran22/nx-module-boundaries-demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Reference: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nx.dev/docs/technologies/eslint/eslint-plugin/guides/enforce-module-boundaries" rel="noopener noreferrer"&gt;https://nx.dev/docs/technologies/eslint/eslint-plugin/guides/enforce-module-boundaries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nx.dev/docs/features/enforce-module-boundaries" rel="noopener noreferrer"&gt;https://nx.dev/docs/features/enforce-module-boundaries&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nx</category>
      <category>architecture</category>
      <category>typescript</category>
      <category>monorepo</category>
    </item>
  </channel>
</rss>
