SalakCode SalakCode
Architecture

Monorepos

Manage multiple packages in one repository

Intermediate
architecture tooling scaling monorepo

Definition

A monorepo is a single repository containing multiple distinct projects with well-defined relationships. It enables code sharing, atomic changes across projects, and unified tooling while maintaining clear boundaries between packages.

Monorepo vs Polyrepo

Polyrepo:                    Monorepo:
┌─────────────┐              ┌──────────────────────────────┐
│  Repo A     │              │         Monorepo             │
│  (App)      │              │  ┌────────┐  ┌────────┐     │
└─────────────┘              │  │ App    │  │ Lib    │     │
                             │  │        │  │        │     │
┌─────────────┐              │  ├────────┤  ├────────┤     │
│  Repo B     │              │  │ Shared │  │ Config │     │
│  (Library)  │              │  │ UI     │  │        │     │
└─────────────┘              │  └────────┘  └────────┘     │
                             └──────────────────────────────┘
                             
Changes across repos          Single commit across packages
Multiple PRs                  One PR
Version management            Atomic changes
Cross-repo breaking changes   Detected immediately

Tools

Nx

// nx.json
{
  "extends": "nx/presets/core.json",
  "npmScope": "myorg",
  "tasksRunnerOptions": {
    "default": {
      "runner": "nx/tasks-runners/default",
      "options": {
        "cacheableOperations": ["build", "test", "lint"]
      }
    }
  }
}
# Generate new app
nx generate @nx/react:app my-app

# Build with caching
nx build my-app

# Run tests for affected projects
nx affected:test

# Graph dependencies
nx graph

Turborepo

// turbo.json
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"]
    },
    "test": {
      "dependsOn": ["build"]
    },
    "lint": {
      "outputs": []
    }
  }
}
# Run tasks with caching
turbo run build test lint

# Filter by package
turbo run build --filter=web...

# Parallel execution
turbo run test --concurrency=4

pnpm Workspaces

// package.json
{
  "name": "monorepo-root",
  "private": true,
  "workspaces": ["packages/*", "apps/*"]
}

// pnpm-workspace.yaml
packages:
  - 'packages/*'
  - 'apps/*'
  - 'tools/*'
# Add dependency to specific package
pnpm add lodash --filter @myorg/shared-ui

# Install all dependencies
pnpm install

# Run script in all packages
pnpm -r run build

Project Structure

my-monorepo/
├── apps/
│   ├── web/                 # Main web app
│   ├── admin/               # Admin dashboard
│   └── mobile/              # Mobile app
├── packages/
│   ├── ui/                  # Shared UI components
│   ├── utils/               # Utility functions
│   ├── config/              # Shared configs (eslint, ts)
│   └── types/               # Shared TypeScript types
├── tools/
│   └── scripts/             # Build scripts
├── package.json
├── pnpm-workspace.yaml
├── turbo.json
└── nx.json

Package Configuration

Shared Package

// packages/ui/package.json
{
  "name": "@myorg/ui",
  "version": "1.0.0",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "scripts": {
    "build": "tsup src/index.ts --format cjs,esm --dts",
    "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
    "lint": "eslint src/",
    "test": "vitest"
  },
  "peerDependencies": {
    "react": "^18.0.0"
  }
}

App Using Package

// apps/web/package.json
{
  "name": "@myorg/web",
  "dependencies": {
    "@myorg/ui": "workspace:*",
    "@myorg/utils": "workspace:*",
    "next": "^14.0.0",
    "react": "^18.0.0"
  }
}

Build Pipeline

Dependency Graph

// Turborepo builds in correct order
// web depends on ui
// ui depends on utils

// Running `turbo run build`:
// 1. Build utils
// 2. Build ui (after utils)
// 3. Build web (after ui)

Caching

// Only rebuild what changed
// Cache hits = instant results

// Remote caching (Turborepo/Nx Cloud)
// Share cache across CI/CD and team

Best Practices

1. Clear Boundaries

// Define public API for each package
// packages/ui/src/index.ts
export { Button } from './Button';
export { Input } from './Input';
// Don't export internals
// export { internalHelper } from './internal'; // ❌

2. Shared Configuration

// packages/eslint-config/index.js
module.exports = {
  extends: ['eslint:recommended', 'plugin:react/recommended'],
  rules: {
    // Shared rules
  }
};

// apps/web/.eslintrc.js
module.exports = {
  extends: ['@myorg/eslint-config']
};

3. Testing Strategy

# Unit tests per package
pnpm -r run test

# Integration tests at app level
nx test web --configuration=integration

# E2E tests
nx e2e web-e2e

4. Versioning

# Changesets (recommended)
npx changeset
# Select packages, describe changes

# Version and publish
npx changeset version
npx changeset publish

Common Challenges

Repository Size

# Use sparse checkouts
git sparse-checkout set apps/web packages/ui

# Shallow clones
git clone --depth 1

# Git LFS for binary files

Permission Management

# CODEOWNERS file
/packages/ui/       @team-ui
/apps/web/          @team-web
/packages/config/   @team-platform

CI/CD Optimization

# .github/workflows/ci.yml
- uses: actions/checkout@v3
  
- uses: pnpm/action-setup@v2
  
- name: Build only affected
  run: npx nx affected:build --base=origin/main
  
- name: Test only affected
  run: npx nx affected:test --base=origin/main
Key Takeaway

Monorepos excel for related projects with shared code. Use Nx or Turborepo for task orchestration and caching, pnpm for fast workspace management, and changesets for versioning. Start simple and add tooling incrementally as your monorepo grows.

Resources

Related Topics