Introduction

Large enterprise monorepos quickly suffer from slow CI/CD pipelines. A full build of every Angular app, library and .NET service on every commit wastes CPU, increases feedback time, and costs money. An Intelligent Build System detects which modules are impacted by a change and builds only those — plus their dependents — while ensuring correctness.

This article explains how to design, implement and operate an intelligent build system for hybrid stacks: Angular (frontend) and .NET (backend). You will get:

The goal: faster developer feedback, lower CI cost, and reliable builds for production deployment.

Problem Statement

In a monorepo with many frontend libs, multiple Angular apps, and several .NET microservices:

We want a system that:

  1. Determines the minimal set of modules needing rebuild for a given commit (impacted set).

  2. Builds and tests only that set (and safe dependents).

  3. Preserves correctness (no missing rebuilds).

  4. Integrates with existing CI/CD and developer workflows.

High-Level Architecture

┌─────────────┐      ┌──────────────┐      ┌───────────────┐
│  Developer  │ ---> │  CI Trigger  │ ---> │ Impact Analyzer│
└─────────────┘      └──────┬───────┘      └──────┬────────┘
                            │                    │
                            ▼                    │
                       ┌───────────┐              │
                       │ Git Diff  │<-------------┘
                       └────┬──────┘
                            │
                            ▼
                  ┌───────────────────────┐
                  │ Module Graph Service  │
                  │ (Angular TS + .NET)   │
                  └─────────┬─────────────┘
                            │
                            ▼
                 ┌──────────────────────────┐
                 │ Build Orchestrator (CI)  │
                 │ (local / remote cache /  │
                 │ distributed executors)   │
                 └─────────┬────────────────┘
                            │
                            ▼
                   ┌─────────────────┐
                   │ Artifact Store  │
                   └─────────────────┘

Key Concepts

How Impact Detection Works

1. File Diff → Affected Files

Get changed files for the commit/PR:

# changed files vs main
git fetch origin main
git diff --name-only origin/main...HEAD

2. File → Module Mapping

Map each changed file to a module. Techniques:

Example module-manifest.json:

{
  "modules": [
    { "name": "ui-button", "globs": ["libs/ui/button/**"] },
    { "name": "orders-api", "globs": ["src/Services/Orders/**"] }
  ]
}

Apply with minimatch or gitignore-style matching.

3. Module Graph Construction

Two build-time graphs required:

dotnet msbuild -nologo -t:GenerateRestoreGraphFile -p:RestoreGraphOutputPath=graph.dg
# or use dotnet list <proj> reference -- include-transitive
dotnet list src/Services/Orders/Orders.csproj reference

Store combined graph in a canonical format (JSON adjacency list). This graph is cached and updated when projects change.

4. Transitive Closure → Impacted Modules

For each module representing a changed file, compute all modules that depend on it (reverse graph traversal). That union is the impacted set.

If module A depends on B and B changed → A is impacted.

Optionally include only direct dependents or include transitive layers; generally transitive closure is safe.

Dealing With APIs And Contracts

Not all changes require rebuilding dependents:

Approaches

  1. Conservative (safe): always rebuild dependents. Simple but might rebuild too much.

  2. Contract Aware (preferred)

    • Use public API extractor (TypeScript api-extractor for libs; .NET PublicApiCompat or ApiPort) to compute API surface hash.

    • If API hash unchanged, skip dependents. If changed, include dependents.

    • For .NET, use dotnet format? Better: use PublicApiAnalyzers or extract public API via reflection/unit tests and compare snapshots.

Example flow:

This enables precise decisions: implementation-only change → no dependent rebuild.

Angular Specifics

Nx (highly recommended)

If not using Nx, implement script:

changed=$(git diff --name-only origin/main...HEAD)
modules=$(node scripts/map-files-to-modules.js $changed)
impacted=$(node scripts/compute-impacted.js $modules graph.json)
for m in $impacted; do ng build --project $m; done

.NET Specifics

Techniques

For interface-aware detection

CI Pipeline Examples

GitHub Actions (simplified)

name: CI
on: [push, pull_request]
jobs:
  analyze:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup Node
        uses: actions/setup-node@v3
      - name: Setup .NET
        uses: actions/setup-dotnet@v3
        with: dotnet-version: '8.0.x'
      - name: Detect changed files
        id: diff
        run: |
          git fetch origin main
          git diff --name-only origin/main...HEAD > changed.txt
          echo "::set-output name=files::$(cat changed.txt | jq -R -s -c 'split("\n")')"
      - name: Map to modules + compute impacted
        id: impacted
        run: |
          node scripts/compute-impacted.js changed.txt graph.json > impacted.json
          echo "::set-output name=modules::$(cat impacted.json)"
  build:
    needs: analyze
    runs-on: ubuntu-latest
    strategy:
      matrix:
        module: ${{ fromJson(needs.analyze.outputs.modules) }}
    steps:
      - uses: actions/checkout@v3
      - name: Build module
        run: |
          if [[ "${{ matrix.module.type }}" == "angular" ]]; then
             ng build --project ${{ matrix.module.name }}
          else
             dotnet build ${{ matrix.module.path }} -c Release
          fi

This pattern parallelizes per module; combine with caching for speed.

Azure DevOps / GitLab

Same idea: run analyzer job and then a dynamic matrix job that builds impacted modules.

Build Cache And Distributed Execution

Building only impacted modules still benefits greatly from build cache & remote execution.

Options

Cache key design: include commit hashes of module sources + relevant dependency API hashes + environment variables.

Tests: Unit, Integration, E2E

Correctness requires tests. Build system should:

Example rule

Use test selection:

Observability And Metrics

Track:

Store metrics in Prometheus/Grafana or cloud CI analytics. Alert on false negatives and increased E2E failures correlated with skipped builds.

Security And Supply Chain Considerations

Operational Playbook

Common Pitfalls And How To Avoid Them

Sample Scripts and Tools

Small Bash example to list changed Angular projects (without Nx):

changed_files=$(git diff --name-only origin/main...HEAD)
projects=()
for projDir in $(jq -r '.projects | keys[]' angular.json); do
  root=$(jq -r ".projects[\"$projDir\"].root" angular.json)
  for f in $changed_files; do
    if [[ $f == $root* ]]; then
      projects+=($projDir)
      break
    fi
  done
done
echo "${projects[@]}"

Roadmap For Maturity

  1. Start: path-based mapping + transitive closure + conservative rebuild.

  2. Add: public API snapshot detection for safer skipping.

  3. Add: build cache + artifact store, parallelized matrix builds.

  4. Add: distributed remote execution (Bazel, BuildGrid) for large orgs.

  5. Add: automatic rollback policies and full-run verification triggers if anomalies detected.

Conclusion

An Intelligent Build System saves time and money while improving developer productivity. The core ingredients are: