Monorepo Structure with Turborepo
Context and Problem Statement​
CellixJS is a complex Domain-Driven Design (DDD) application built on Azure Functions with multiple interconnected packages including domain models, API layers, persistence, GraphQL, and UI components. Initially, the codebase was organized in a way that made it difficult to manage dependencies, optimize builds, and maintain consistent development workflows across packages. We needed a scalable approach to:
- Manage shared code and dependencies across multiple packages
- Optimize build and test performance
- Maintain consistent tooling and configurations
- Enable efficient CI/CD pipelines
- Support both frontend and backend development workflows
The challenge was to choose between maintaining separate repositories or consolidating into a monorepo, and selecting appropriate tooling for build orchestration and caching.
Decision Drivers​
- Build Performance: Need for fast incremental builds and efficient caching
- Dependency Management: Shared code and consistent dependency versions across packages
- Developer Experience: Unified development environment and tooling
- CI/CD Efficiency: Selective builds and caching in automated pipelines
- Code Organization: Clear separation between application and library packages, with logical grouping
- Scalability: Ability to add new packages without architectural overhead
Considered Options​
- NPM Workspaces: Leverage NPM workspaces with typescript project references. Each package in
packages/directory - Monorepo with Turborepo: Focused build orchestration and caching tool. Repository structure following turborepo conventions
Decision Outcome​
We will use a monorepo structure with Turborepo for build orchestration, caching, and task management. The repository is organized with:
packages/directory containing shared libraries and domain-specific packagesapps/directory containing deployable applications- Turborepo configuration for selective builds and dependency management
- npm workspaces for package management
See the following Turborepo documentation for reference.
Consequences​
Positive​
- Improved Build Performance: Turborepo's intelligent caching reduces build times by 50-80% for unchanged packages
- Selective Execution: Only affected packages and their dependents are built/tested
- Unified Development Environment: Single repository with consistent tooling and configurations
- Efficient CI/CD: Azure Pipelines integration with selective builds and remote caching
- Better Dependency Management: npm workspaces ensure consistent dependency versions
- Clear Code Organization: Logical separation between domain, application, and infrastructure concerns
Negative​
- Increased Repository Size: All packages in one repository can lead to larger clones
- Complex Initial Setup: Turborepo configuration requires careful task dependency management
- Learning Curve: Team needs to understand Turborepo concepts and selective execution
- Potential for Tighter Coupling: Monorepo can encourage tighter coupling between packages
Validation​
The monorepo structure and Turborepo usage is validated through:
- Build Performance Metrics: Local builds complete in ~160ms for cached packages
- CI/CD Pipeline Efficiency: PR builds are 30-60% faster due to selective execution
- Code Quality Gates: All packages maintain consistent linting, testing, and coverage standards
- Dependency Resolution: npm workspaces ensure no version conflicts between packages
Pros and Cons of the Options​
NPM Workspaces​
- Good: Built-in to npm, no additional tooling required
- Good: Simple setup with package.json workspaces field
- Neutral: Basic support for linking local packages
- Bad: No advanced build orchestration or caching
- Bad: Limited to package management only
Monorepo with Turborepo​
- Good: Excellent build performance with intelligent caching
- Good: Simple configuration and focused on build orchestration
- Good: Seamless CI/CD integration with remote caching
- Good: Fast and reliable for our package structure
- Neutral: Less code generation features compared to Nx
- Bad: Limited to build/task orchestration (no code generation)
Repository Structure​
The monorepo follows this structure:
packages/ # Shared libraries
├── cellix/ # Shared seedwork libraries for CellixJS applications
│ ├── domain-seedwork/ # Domain modeling base classes
│ ├── typescript-config/ # Shared TypeScript configurations
│ ├── vitest-config/ # Shared testing configurations
│ └── mock-* # Mocking utilities for local development
│ └── ui-core/ # Shared UI components
│ └── ...
├── ocom/ # Application-specific packages (OwnerCommunity)
│ ├── domain/ # Domain models and business logic
│ ├── graphql/ # GraphQL schema and resolvers
│ ├── persistence/ # Data access layer
│ └── service-* # Infrastructure services
| └── ...
apps/ # Deployable applications
├── api/ # Azure Functions application (Backend)
├── docs/ # Documentation site (Docusaurus)
└── ui-community/ # React UI application (Frontend)
Turborepo Configuration​
Task Dependencies​
Turborepo manages task execution with dependency graphs:
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", "build/**"]
},
"test": {
"dependsOn": ["^build"]
}
}
}
Selective Execution​
- Local Development:
turbo run buildbuilds only affected packages - CI/CD: Azure Pipelines detects changes and builds selectively
- Caching: Local
.turbodirectory and remote Azure Cache@2
Package Categories​
Packages are categorized by tags for selective deployments:
- frontend: UI applications and components
- backend: API, domain, and infrastructure packages
- mock: Local development utilities
- config: Shared configurations
- docs: Documentation