Skip to Content
Lessons From Shipping Production Monorepos

Lessons From Shipping Production Monorepos

  • monorepo
  • pnpm
  • typescript
  • software-architecture
  • devops
  • turborepo
4 min read Ritik Tiwari

Monorepos that actually work in production are very different from the toy examples you usually see online.

After shipping multiple production systems using pnpm workspaces — including multi-app platforms spanning web clients, backend APIs, CLIs, shared packages and AI services — I’ve learned the hard way where monorepos shine, and where they hurt.

This is the advice I’d give myself before starting.

Why a Monorepo at All?

The pitch is compelling:

  • Shared types
  • Shared utilities
  • Atomic cross-package changes
  • Unified CI/CD
  • Consistent tooling

And those benefits are real. But so are the tradeoffs. Monorepos shift complexity left — you deal with integration problems upfront instead of discovering them months later. That is often worth it.

The Dependency Graph Becomes Your Mental Model

In a single-package app, you import a file. In a monorepo, you import a package.

That package has:

  • Build artifacts
  • Types
  • Dependencies
  • Peer dependencies
  • Versioning concerns

Forgetting this causes broken builds. A lot. Use recursive builds or filtered builds deliberately:

pnpm -r build

or

pnpm --filter "./packages/*" run build

Shared packages should build first. Bake that into local development and CI.

Package Boundaries Matter More Than Folder Structure

A monorepo is not: “One repository with lots of folders.” It’s a dependency graph.

Bad package boundaries:

packages/
  shared/
  utils/
  helpers/
  common/

Those become junk drawers.

Better:

packages/
  ui/
  config/
  sdk/
  domain/
  logger/

Organize around responsibilities, not vague reuse. That scales much better.

TypeScript Path Aliases Are a Footgun

Everyone wants:

@/components/Button

Across package boundaries, this causes subtle pain. Especially with:

  • Tests
  • Editors
  • Build tooling
  • Package publishing

Keep aliases local.

Example:

{
	"compilerOptions": {
		"paths": {
			"@ui/*": ["./src/*"]
		}
	}
}

Consumers should import packages:

import { Button } from "@your-scope/ui";

not internal alias paths. That distinction matters.

Peer Dependencies Will Bite You

One thing pnpm does very well — it exposes bad dependency assumptions. Reusable packages often accidentally put framework dependencies in dependencies instead of peerDependencies.

Bad:

{
	"dependencies": {
		"react": "^18"
	}
}

Better:

{
	"peerDependencies": {
		"react": "^18"
	}
}

This prevents duplicate framework installs and weird runtime bugs. I learned this one painfully.

Circular Dependencies Hide Until They Hurt

Monorepos make circular dependencies easier than you think.

Example:

app -> ui -> utils -> app

Now your dependency graph is lying. Use tooling to catch it early:

  • Madge
  • dependency-cruiser
  • Turborepo graph inspection

Circular imports get expensive later.

Consider Turborepo Early

Manual filter chains work until they don’t. Turborepo solves problems you’ll eventually hit:

  • Task pipelines
  • Caching
  • Incremental builds
  • Affected-only rebuilds

Example:

{
	"pipeline": {
		"build": {
			"dependsOn": ["^build"]
		}
	}
}

That ^build dependency chain is gold. If I were starting over, I’d likely adopt it from day one.

Shared Configs Should Be Packages

Don’t copy-paste:

  • ESLint configs
  • TypeScript configs
  • Prettier rules

Package them.

Example:

packages/
  eslint-config/
  tsconfig/

Then consume them everywhere. Drift is sneaky. Centralization helps.

Version Internal Packages Early

Even if packages are private.

Versioning forces:

  • Clear interfaces
  • Better change discipline
  • Safer refactors

And if you later adopt Changesets or publishing, you’re ready. I wish I had done this earlier.

CI Is Where Monorepos Become Real

A monorepo isn’t validated by local dev. It’s validated by CI. At minimum:

  • Cache dependencies
  • Cache builds
  • Run affected tasks only
  • Build packages before apps

Otherwise CI becomes painfully slow. And developers stop trusting it.

What I’d Do Differently

If I started over:

  1. Start with stronger package boundaries
  2. Adopt Turborepo earlier
  3. Treat peer dependencies seriously from day one
  4. Package shared configs immediately
  5. Add circular dependency detection early

All five would have saved me days. Maybe weeks.

When I Would Not Use a Monorepo

I wouldn’t force one for:

  • Tiny projects
  • One-off prototypes
  • Small teams with one deployable
  • Products with no shared code

Sometimes a single repo should just stay simple. Monorepos are leverage — but not free leverage.

My Personal Bias

If I were choosing by default:

Monorepo is usually worth it for…I’d think twice for…
Multi-app platformsSmall single-service apps
Shared SDKs + APIsThrowaway prototypes
Web + mobile productsTiny teams
Platform engineeringSimple CRUD MVPs
Internal tooling ecosystemsOne deployable product

Context always matters.

Final Thoughts

Monorepos compound leverage — and complexity. If done poorly, they become dependency spaghetti. If done well, they make multi-app development feel like one system. I’d still choose a monorepo again. Just with much better boundaries.

Happy building 🚀


Related Posts