ð monorepo-architecture
Use when designing monorepo structure, organizing packages, or migrating to monorepo architecture with architectural patterns for managing dependencies and scalable workspace configurations.
Overview
Overview
This skill provides comprehensive guidance on designing and structuring monorepos, including workspace organization, dependency management, versioning strategies, and architectural patterns that scale from small projects to enterprise applications.
Monorepo vs Polyrepo
When to Choose Monorepo
A monorepo is beneficial when:
- Code sharing is frequent: Multiple projects share common libraries, utilities, or components
- Atomic changes needed: Changes span multiple packages and need to be deployed together
- Unified tooling: All projects benefit from consistent linting, testing, and build processes
- Team collaboration: Teams work across project boundaries and need visibility into related code
- Version synchronization: Related packages should maintain version alignment
- Refactoring at scale: Large-scale refactoring across projects is common
- Single source of truth: All code, documentation, and tooling in one place
When to Choose Polyrepo
A polyrepo is beneficial when:
- Independent release cycles: Projects deploy on completely different schedules
- Different tech stacks: Projects use incompatible tooling or languages
- Access control: Different teams need isolated access to separate codebases
- Repository size concerns: Combined codebase would be too large to manage efficiently
- External dependencies: Projects are maintained by different organizations
- Simple project structure: Overhead of monorepo tooling outweighs benefits
Tradeoffs
Monorepo Advantages:
- Simplified dependency management
- Easier refactoring across boundaries
- Consistent tooling and standards
- Better code discoverability
- Atomic commits across projects
- Single CI/CD pipeline
Monorepo Challenges:
- Repository size growth
- CI/CD complexity
- Build time management
- Git performance at scale
- Tooling requirements
- Learning curve for developers
Repository Structure Patterns
Package-Based Structure
Organize by technical layer or package type.
my-monorepo/
âââ apps/
â âââ web/ # Next.js web application
â âââ mobile/ # React Native app
â âââ api/ # Node.js API server
âââ packages/
â âââ ui/ # Shared UI components
â âââ utils/ # Shared utilities
â âââ config/ # Shared configurations
â âââ types/ # Shared TypeScript types
âââ services/
â âââ auth/ # Authentication service
â âââ payments/ # Payment processing
â âââ notifications/ # Notification service
âââ tooling/
âââ eslint-config/ # ESLint configuration
âââ tsconfig/ # TypeScript configuration
Best for: Technical separation, shared library focus, platform diversity.
Domain-Based Structure
Organize by business domain or feature.
my-monorepo/
âââ domains/
â âââ user/
â â âââ api/ # User API
â â âââ web/ # User web UI
â â âââ mobile/ # User mobile UI
â â âââ shared/ # User shared code
â âââ billing/
â â âââ api/
â â âââ web/
â â âââ shared/
â âââ analytics/
â âââ api/
â âââ web/
â âââ shared/
âââ shared/
âââ ui/ # Cross-domain UI components
âââ utils/ # Cross-domain utilities
âââ config/ # Cross-domain config
Best for: Domain-driven design, team ownership by feature, microservices architecture.
Hybrid Structure
Combine package-based and domain-based approaches.
my-monorepo/
âââ apps/
â âââ customer-portal/ # Customer-facing app
â âââ admin-dashboard/ # Admin app
âââ features/
â âââ auth/ # Authentication feature
â âââ checkout/ # Checkout feature
â âââ inventory/ # Inventory feature
âââ packages/
â âââ ui/ # Shared UI library
â âââ api-client/ # API client library
â âââ analytics/ # Analytics library
âââ infrastructure/
âââ database/ # Database utilities
âââ messaging/ # Message queue
âââ deployment/ # Deployment configs
Best for: Complex organizations, balancing technical and domain concerns.
Workspace Configuration
NPM Workspaces
Basic workspace setup using native NPM workspaces.
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"apps/*",
"packages/*"
],
"scripts": {
"dev": "npm run dev --workspaces",
"build": "npm run build --workspaces",
"test": "npm run test --workspaces"
},
"devDependencies": {
"typescript": "^5.3.0",
"eslint": "^8.54.0"
}
}
Key Features:
- Native NPM support (v7+)
- Simple configuration
- Automatic workspace linking
- Shared dependency hoisting
- Workspace-specific commands
Yarn Workspaces
Yarn's workspace implementation with additional features.
{
"name": "my-monorepo",
"private": true,
"workspaces": {
"packages": [
"apps/*",
"packages/*"
],
"nohoist": [
"**/react-native",
"**/react-native/**"
]
},
"packageManager": "yarn@3.6.4"
}
With .yarnrc.yml for Yarn Berry:
nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: "@yarnpkg/plugin-workspace-tools"
enableGlobalCache: true
compressionLevel: mixed
Key Features:
- Fast installation
- Plugin system (Yarn Berry)
- Advanced workspace commands
- No-hoist for specific dependencies
- Zero-installs capability
PNPM Workspaces
PNPM's efficient workspace implementation.
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
- 'services/*'
- '!**/test/**'
With workspace-specific .npmrc:
# .npmrc
shared-workspace-lockfile=true
link-workspace-packages=true
prefer-workspace-packages=true
strict-peer-dependencies=false
auto-install-peers=true
Key Features:
- Content-addressable storage
- Strict node_modules structure
- Faster than NPM/Yarn
- Efficient disk space usage
- Built-in monorepo support
Cargo Workspaces (Rust)
Rust monorepo workspace configuration.
# Cargo.toml (root)
[workspace]
members = [
"crates/core",
"crates/api",
"crates/cli",
]
exclude = ["archived/*"]
[workspace.package]
version = "1.0.0"
edition = "2021"
license = "MIT"
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.35", features = ["full"] }
Individual crate:
# crates/core/Cargo.toml
[package]
name = "my-core"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
serde.workspace = true
tokio.workspace = true
my-api = { path = "../api" }
Dependency Management
Internal Package Dependencies
Specify dependencies on other workspace packages.
{
"name": "@myorg/web-app",
"version": "1.0.0",
"dependencies": {
"@myorg/ui": "workspace:*",
"@myorg/utils": "workspace:^",
"@myorg/api-client": "1.2.3",
"react": "^18.2.0"
}
}
Workspace Protocol Variants:
workspace:*- Any version in workspaceworkspace:^- Compatible version (semver caret)workspace:~- Patch-level version (semver tilde)- Specific version - Exact workspace version
Shared Dependencies Hoisting
Configure how dependencies are hoisted to root.
{
"name": "my-monorepo",
"workspaces": {
"packages": ["packages/*"],
"nohoist": [
"**/react-native",
"**/react-native/**",
"**/@babel/**"
]
}
}
Hoisting Strategies:
- Full hoisting: All common dependencies at root (default)
- Selective hoisting: Specific packages hoisted, others isolated
- No hoisting: Each package has isolated dependencies
- Public hoisting: Only public dependencies hoisted
Version Synchronization
Keep related dependencies in sync across packages.
{
"name": "my-monorepo",
"private": true,
"syncpack": {
"semverGroups": [
{
"range": "",
"dependencies": ["react", "react-dom"],
"packages": ["**"]
}
],
"versionGroups": [
{
"label": "React ecosystem must match",
"dependencies": ["react", "react-dom"],
"dependencyTypes": ["prod", "dev"],
"pinVersion": "18.2.0"
}
]
}
}
Use tools like syncpack to enforce consistency:
# Check for version inconsistencies
pnpm syncpack list-mismatches
# Fix version inconsistencies
pnpm syncpack fix-mismatches
# Update all versions
pnpm syncpack update
Peer Dependencies in Monorepos
Handle peer dependencies correctly across workspace packages.
{
"name": "@myorg/ui",
"version": "1.0.0",
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
},
"devDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
Best Practices:
- Declare peer dependencies in shared libraries
- Include peer deps as dev dependencies for testing
- Use
peerDependenciesMetafor optional peers - Document peer dependency requirements
- Test with minimum and maximum peer versions
Code Organization
Shared Code Patterns
Organize shared code for maximum reusability.
packages/
âââ ui/
â âââ src/
â â âââ components/ # Reusable components
â â âââ hooks/ # Custom React hooks
â â âââ styles/ # Shared styles
â â âââ index.ts # Public API
â âââ package.json
âââ utils/
â âââ src/
â â âââ string/ # String utilities
â â âââ date/ # Date utilities
â â âââ validation/ # Validation functions
â â âââ index.ts # Public API
â âââ package.json
âââ config/
âââ eslint-config/
âââ tsconfig/
âââ prettier-config/
Shared Library Design:
- Clear public API via barrel exports
- Minimal external dependencies
- Well-documented interfaces
- Comprehensive unit tests
- Semantic versioning
- Changelog maintenance
Type Sharing Across Packages
Share TypeScript types efficiently.
// packages/types/src/user.ts
export interface User {
id: string;
email: string;
name: string;
role: UserRole;
}
export enum UserRole {
Admin = 'admin',
User = 'user',
Guest = 'guest',
}
export type CreateUserInput = Omit<User, 'id'>;
export type UpdateUserInput = Partial<CreateUserInput>;
// packages/types/package.json
{
"name": "@myorg/types",
"version": "1.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./user": {
"types": "./dist/user.d.ts",
"default": "./dist/user.js"
}
}
}
Configuration Sharing
Share build and tooling configuration across packages.
// packages/tsconfig/base.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
}
}
// apps/web/tsconfig.json
{
"extends": "@myorg/tsconfig/base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"],
"references": [
{ "path": "../../packages/ui" },
{ "path": "../../packages/utils" }
]
}
Build Dependencies
Dependency Graphs
Understand and visualize package dependencies.
{
"name": "@myorg/web",
"dependencies": {
"@myorg/ui": "workspace:*",
"@myorg/api-client": "workspace:*"
}
}
Generate dependency graph:
# Using pnpm
pnpm list --depth 10 --json > deps.json
# Using Nx
nx graph
# Using custom script
node scripts/generate-dep-graph.js
Build Order Optimization
Ensure packages build in correct dependency order.
{
"name": "my-monorepo",
"scripts": {
"build": "turbo run build",
"build:order": "pnpm -r --workspace-concurrency=1 run build"
}
}
Turbo pipeline configuration:
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
}
}
}
Dependency Resolution:
^build- Build dependencies firstdependsOn- Explicit task dependencies- Topological sorting for correct order
- Parallel execution when safe
Circular Dependency Detection
Prevent and detect circular dependencies.
// scripts/check-circular-deps.js
import madge from 'madge';
async function checkCircularDeps() {
const result = await madge('src', {
fileExtensions: ['ts', 'tsx'],
detectiveOptions: {
ts: { skipTypeImports: true }
}
});
const circular = result.circular();
if (circular.length > 0) {
console.error('Circular dependencies detected:');
circular.forEach(cycle => {
console.error(cycle.join(' -> '));
});
process.exit(1);
}
}
checkCircularDeps();
Add to CI pipeline:
# .github/workflows/ci.yml
- name: Check circular dependencies
run: pnpm check:circular
Versioning Strategies
Independent Versioning
Each package has its own version, released independently.
{
"name": "@myorg/ui",
"version": "2.1.0"
}
{
"name": "@myorg/utils",
"version": "1.5.3"
}
Advantages:
- Fine-grained version control
- Independent release cycles
- Clear package maturity
- Semantic versioning per package
Use when:
- Packages have different stability levels
- Release frequency varies significantly
- Packages serve different purposes
Fixed/Locked Versioning
All packages share the same version number.
{
"name": "@myorg/ui",
"version": "3.2.0"
}
{
"name": "@myorg/utils",
"version": "3.2.0"
}
Advantages:
- Simplified version management
- Clear release coordination
- Easier to track compatibility
- Unified changelog
Use when:
- Packages are tightly coupled
- All packages release together
- Single product with multiple packages
- Version sync is critical
Semantic Versioning in Monorepos
Apply semver principles to workspace packages.
Breaking Changes (Major):
- Change public API signatures
- Remove exported functions
- Change function behavior significantly
- Update peer dependencies with breaking changes
New Features (Minor):
- Add new exports
- Add optional parameters
- Enhance existing functionality
- Add new optional features
Bug Fixes (Patch):
- Fix bugs without API changes
- Update documentation
- Refactor internal implementation
- Update dependencies (non-breaking)
Best Practices
1. Clear Package Boundaries
Define explicit boundaries and responsibilities for each package.
Implementation:
- Document package purpose and scope
- Define public API explicitly
- Use barrel exports (
index.ts) - Minimize cross-package coupling
- Review package boundaries regularly
Example:
// packages/ui/src/index.ts - Clear public API
export { Button } from './components/Button';
export { Input } from './components/Input';
export type { ButtonProps, InputProps } from './types';
// Internal implementation details NOT exported
// ./components/Button/ButtonStyles.ts
// ./utils/internal-helper.ts
2. Minimal Coupling Between Packages
Reduce dependencies between packages to maintain flexibility.
Implementation:
- Use dependency injection
- Prefer composition over inheritance
- Define clear interfaces
- Avoid deep dependency chains
- Use events/messaging for loose coupling
Example:
// Loose coupling via interfaces
interface Logger {
log(message: string): void;
}
class PaymentService {
constructor(private logger: Logger) {}
processPayment(amount: number) {
this.logger.log(`Processing payment: ${amount}`);
// Implementation
}
}
3. Shared Tooling Configuration
Maintain consistent tooling across all packages.
Implementation:
- Create shared config packages
- Extend base configurations
- Use workspace inheritance
- Document configuration decisions
- Automate configuration validation
Example:
// packages/eslint-config/index.json
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"no-console": "warn",
"@typescript-eslint/no-unused-vars": "error"
}
}
4. Consistent Naming Conventions
Use predictable naming patterns across packages.
Implementation:
- Scope all packages (
@org/package-name) - Use kebab-case for package names
- Prefix related packages consistently
- Follow language/ecosystem conventions
- Document naming standards
Example:
@myorg/web-app
@myorg/mobile-app
@myorg/ui-components
@myorg/api-client
@myorg/utils-date
@myorg/utils-string
@myorg/config-eslint
@myorg/config-typescript
5. Documentation Standards
Maintain comprehensive documentation for all packages.
Implementation:
- README in every package
- API documentation
- Usage examples
- Migration guides
- Contribution guidelines
Example:
# @myorg/ui
React component library for MyOrg applications.
## Installation
`pnpm add @myorg/ui`
## Usage
import { Button } from '@myorg/ui';
<Button onClick={handleClick}>Click me</Button>
## API Reference
See [API.md](./API.md) for detailed documentation.
6. Package Ownership
Assign clear ownership and responsibility for packages.
Implementation:
- CODEOWNERS file
- Package maintainer documentation
- Review process for changes
- Communication channels
- Ownership rotation plan
Example:
# CODEOWNERS
/packages/ui/ @frontend-team
/packages/api-client/ @api-team
/packages/auth/ @security-team
/services/ @backend-team
7. API Contracts
Define and maintain clear API contracts between packages.
Implementation:
- TypeScript interfaces
- OpenAPI specifications
- JSON Schema
- Contract testing
- Version compatibility matrix
Example:
// packages/api-client/src/contracts.ts
/**
* User API contract
* @version 1.0.0
*/
export interface UserAPI {
getUser(id: string): Promise<User>;
createUser(data: CreateUserInput): Promise<User>;
updateUser(id: string, data: UpdateUserInput): Promise<User>;
deleteUser(id: string): Promise<void>;
}
8. Migration Strategies
Plan for package updates and breaking changes.
Implementation:
- Deprecation warnings
- Migration guides
- Compatibility layers
- Automated migrations (codemods)
- Version upgrade paths
Example:
// Deprecation with migration path
/**
* @deprecated Use `getUser` instead
* This function will be removed in v3.0.0
*/
export function fetchUser(id: string): Promise<User> {
console.warn('fetchUser is deprecated, use getUser instead');
return getUser(id);
}
9. Security Boundaries
Maintain security isolation between packages.
Implementation:
- Separate sensitive packages
- Access control policies
- Security scanning per package
- Dependency audit
- Secret management
Example:
{
"scripts": {
"security:audit": "pnpm audit --audit-level=high",
"security:check": "pnpm dlx audit-ci --high"
}
}
10. Testing Isolation
Ensure tests don't have unintended dependencies.
Implementation:
- Package-level test configuration
- Mock workspace dependencies
- Integration test suites
- CI test isolation
- Test data management
Example:
// packages/ui/src/__tests__/Button.test.tsx
import { render, screen } from '@testing-library/react';
import { Button } from '../Button';
// Test isolated from other packages
describe('Button', () => {
it('renders correctly', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
});
Common Pitfalls
1. Tight Coupling Between Packages
Creating excessive dependencies between packages.
Symptoms:
- Changes require updates across many packages
- Difficult to extract or move packages
- Long dependency chains
- Circular dependencies
Solution:
- Use dependency injection
- Define clear interfaces
- Implement event-driven communication
- Regular refactoring to reduce coupling
2. Unclear Package Responsibilities
Packages with overlapping or undefined purposes.
Symptoms:
- Duplicate functionality
- Uncertain where to add features
- Inconsistent implementations
- Code duplication
Solution:
- Document package purpose clearly
- Single Responsibility Principle
- Regular architecture reviews
- Refactor to clarify boundaries
3. Inconsistent Dependency Versions
Different versions of same dependency across packages.
Symptoms:
- Build errors
- Runtime conflicts
- Peer dependency warnings
- Bundle size bloat
Solution:
- Use workspace protocol
- Implement syncpack or similar
- Centralize version management
- CI checks for consistency
4. Poor Build Optimization
Not leveraging caching and incremental builds.
Symptoms:
- Slow build times
- Rebuilding unchanged packages
- Long CI pipeline runs
- Developer frustration
Solution:
- Implement build caching (Turborepo, Nx)
- Use affected analysis
- Configure incremental builds
- Optimize build pipelines
5. Missing Documentation
Inadequate documentation for packages and APIs.
Symptoms:
- Frequent questions about usage
- Incorrect usage patterns
- Difficult onboarding
- Knowledge silos
Solution:
- README in every package
- API documentation generation
- Usage examples
- Onboarding guides
6. Monolithic Thinking in Monorepo
Treating monorepo as single large application.
Symptoms:
- Shared global state
- Tightly coupled code
- Difficult to extract packages
- No clear package boundaries
Solution:
- Design packages as independent units
- Minimize shared global state
- Clear separation of concerns
- Regular boundary reviews
7. Over-Sharing Code
Sharing code that should remain private.
Symptoms:
- Implementation details exposed
- Brittle dependencies
- Difficult to refactor
- Version management complexity
Solution:
- Use barrel exports for public API
- Keep internals private
- Document public vs private
- Review exports regularly
8. Ignoring Package Boundaries
Importing from package internals instead of public API.
Symptoms:
- Brittle imports
- Breaking changes on refactor
- Unclear dependencies
- Type errors
Solution:
- Only import from package root
- Configure linting rules
- Use TypeScript project references
- Code review enforcement
9. No Versioning Strategy
Lack of clear versioning approach.
Symptoms:
- Unclear compatibility
- Breaking changes without warning
- Difficult rollbacks
- Consumer confusion
Solution:
- Choose fixed or independent versioning
- Use changesets or conventional commits
- Semantic versioning discipline
- Automated version management
10. Complex Dependency Chains
Deep or circular dependency relationships.
Symptoms:
- Difficult to understand flow
- Build order issues
- Circular dependency errors
- Maintenance difficulty
Solution:
- Visualize dependency graph
- Refactor to reduce depth
- Break circular dependencies
- Use dependency injection
When to Use This Skill
Apply monorepo architecture principles when:
- Designing new monorepos - Setting up structure and organization
- Refactoring existing repos - Improving architecture and organization
- Migrating to monorepo - Moving from polyrepo to monorepo
- Scaling monorepo - Growing from small to large monorepo
- Organizing packages - Deciding package boundaries and structure
- Managing dependencies - Handling internal and external dependencies
- Establishing patterns - Creating architectural standards
- Reviewing architecture - Evaluating existing monorepo structure
- Troubleshooting issues - Solving dependency or organization problems
- Planning refactors - Restructuring packages or dependencies
Resources
- Monorepo.tools - Comprehensive comparison of monorepo tools and patterns
- Turborepo Handbook - Best practices for Turborepo architecture
- Nx Monorepo Guide - Architectural concepts and patterns
- PNPM Workspaces - Documentation for PNPM workspace features
- Yarn Workspaces - Yarn workspace implementation guide
- Lerna Documentation - Multi-package repository management
- Bazel Documentation - Build and test system for large monorepos
- Rush Documentation - Scalable monorepo manager for JavaScript