This is Part 3 of a series on plan-based development with Claude Code. Today we explore how Turborepo makes our massive monorepo manageable.
Why Build Orchestration Matters
In a monorepo with 60+ packages, the naive approach to building and testing is brutally slow. Run every test? That’s 20 minutes. Build every package? Another 15. Type-check everything? Add another 10.
Multiply these numbers by the frequency of commits, and you have a recipe for slow iteration cycles and frustrated developers.
Turborepo solves this with intelligent caching and parallel execution. But the real power comes from how you configure it.
Our Turborepo Configuration Philosophy
Our turbo.json evolved through many iterations. Here’s the philosophy behind our current setup:
1. Declare Dependencies Explicitly
{ "tasks": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**", ".next/**", "!.next/cache/**"] }, "check-types": { "dependsOn": ["build", "^build"], "inputs": ["$TURBO_DEFAULT$", "tsconfig.json"] }, "dev": { "dependsOn": ["^build"], "persistent": true, "interruptible": true } }}The ^build syntax means “build my dependencies first.” This ensures packages are built in the correct order without manual coordination.
2. Precise Input Definitions
{ "build": { "inputs": [ "$TURBO_DEFAULT$", ".env*", "!**/*.test.{ts,tsx}", "!**/*.spec.{ts,tsx}", "!coverage/**", "!storybook-static/**" ] }}By excluding test files and coverage reports from build inputs, we avoid rebuilding packages when only tests change. This seemingly small optimization saves significant time across the full repository.
3. Package Boundaries
One of Turborepo’s powerful features is boundary enforcement:
{ "boundaries": { "tags": { "db": { "dependencies": { "deny": ["app", "ui", "ai"] } }, "api": { "dependencies": { "deny": ["app", "ui", "ai"] } }, "ui": { "dependencies": { "deny": ["app"] } } } }}This configuration enforces architectural rules:
- Database packages can’t depend on UI or AI packages
- API packages are pure logic, no UI dependencies
- UI packages don’t directly depend on apps
When someone accidentally imports a UI component into a database package, turbo boundaries catches it immediately.
Real Performance Numbers
Before and after Turborepo optimization:
| Operation | Before | After | Improvement |
|---|---|---|---|
| Full build (cold) | 4m 30s | 4m 30s | 0% (same) |
| Full build (cached) | 4m 30s | 8s | 97% |
| Type check (cached) | 3m 15s | 12s | 94% |
| Test (affected) | 8m | 45s | 91% |
| Dev startup | 90s | 15s | 83% |
The key insight: Turborepo’s value isn’t in the first run—it’s in every subsequent run. Most of your development time is incremental, not from-scratch.
Package-Level Configuration
Each package has its own turbo.json that extends the root:
{ "$schema": "https://turbo.build/schema.json", "extends": ["//"], "tags": ["db"], "tasks": { "build": { "env": ["DATABASE_URL", "DB_*", "DATABASE_MODE"], "outputs": ["dist/**"] } }}This allows packages to declare their specific environment variable dependencies. When DATABASE_URL changes, only packages that depend on it rebuild.
The Feedback Loop Impact
Fast builds enable a different kind of development:
- Experiment freely: Quick validation means you try more ideas
- Catch errors early: Run
bun run check-typesafter every significant change - Stay in flow: No 10-minute builds breaking your concentration
- Review faster: CI completes in minutes, not hours
The mantra we’ve adopted: If verification takes less than 30 seconds, you’ll do it constantly. If it takes more than 5 minutes, you’ll skip it.
Turborepo puts most verifications in that under-30-second category.
Next up in Part 4: TypeScript strict mode as your first line of defense.