2026-01-08 | PreviewProof Team

Monorepo vs. Polyrepo for Preview Environments: Tradeoffs and Patterns

monorepopolyrepopreview environmentsNxTurborepobuild tools

Monorepo versus polyrepo gets confused with monolith versus microservices. They’re not the same axis. You can have a monorepo full of independently deployable services, or a polyrepo where everything ships as one binary. The choice is about how source is stored and versioned, not how the system runs at runtime. But repo strategy meaningfully shapes how preview environments work.

The short version: monorepos make it easy to coordinate cross-service preview deployments from a single PR but add build-graph complexity. Polyrepos make change scope obvious but require explicit coordination across PRs that span multiple services. Both work. Neither is free.

What changes when the repo strategy changes

A preview is conceptually “deploy this version of the system.” The interesting part is what “this version” means when a change touches multiple services.

In a monorepo, a PR can include changes to the API, worker, frontend, and shared types library all at once. Building a preview is a single coherent operation — check out one ref, build affected services, deploy together. The preview by definition includes a consistent set of versions.

In a polyrepo, the same change spans multiple PRs across multiple repos. The frontend PR depends on a not-yet-merged API change. The preview needs to know the API isn’t on main anymore — it’s on a specific branch in a specific repo. Without coordination, the preview deploys merged-main API (frontend broken) or fails entirely.

Monorepos trade build-system complexity for coordinated previews. Polyrepos trade coordination complexity for clean service boundaries.

Monorepo affordances and pitfalls

The monorepo case for previews is strongest when you have a build tool that understands the dependency graph. Without one, every PR rebuilds and redeploys every service. With one, only affected services rebuild — and the preview includes only services that need to change, sharing the rest from the last known-good baseline.

Nx and Turborepo dominate JavaScript-land. Both maintain an explicit dependency graph, both compute “what changed” from a base ref, both integrate with caching backends.

Terminal window
# Turborepo — what services changed since main?
turbo run build --filter=...[origin/main]
# Nx — same idea
nx affected:build --base=origin/main --head=HEAD

For Go, Bazel is the heavyweight option, with Pants as a more ergonomic alternative. For mixed-language monorepos, Bazel and Buck2 at the cost of significant setup. Smaller teams often skip the build tool and use a paths filter in CI — workable but coarser.

The affordances: a single PR deploys any combination of services, all consistent; cross-service refactors (renaming a shared API contract) ship as one change; the preview’s compose file is a single source of truth.

The pitfalls: build graphs get expensive without good caching; test failures in one service can block deploys of unrelated services unless you’re disciplined; per-service deployment ownership gets fuzzy when 4 teams contribute to one PR.

The pattern that works: define services explicitly in the build graph, scope CI to affected services, and treat the preview as a deployment of the full system with affected services replaced. Unaffected services come from a baseline image (typically the last successful main build), so reviewers always see a complete app even when the PR only changed the frontend.

Polyrepo affordances and pitfalls

Polyrepo previews are easier to reason about for a single service. A PR in the auth-service repo deploys an auth-service preview. The build is fast, the change scope is obvious, no monorepo build tool to maintain.

Where it gets hard is multi-service work. The frontend depends on a new API endpoint still in PR. There’s no single ref that captures both changes.

The pattern that works is a deployment manifest — configuration in the PR that specifies which versions of other services this preview should run against.

# preview-overrides.yaml in the frontend PR
overrides:
api: org/api-service#feat/new-endpoint
worker: org/worker-service#main

The preview machinery reads this file and resolves each service to a specific git ref before building. When the API PR merges, the manifest gets updated to point at main, or stripped as part of cleanup.

Variations: GitHub PR labels of the form api:feat/new-endpoint, comment commands (/preview-with api:feat/new-endpoint), or coordination through Argo Rollouts. Same principle — explicitly capture cross-repo coordination in a place the preview infrastructure can read.

The affordances: build pipelines stay simple with per-repo CI and ownership, service boundaries stay sharp, repos can use different stacks and cadences, failure isolation is excellent.

The pitfalls: cross-service changes require manual coordination engineers have to remember; reviewers may not realize a frontend PR depends on an unmerged API PR; a frontend PR that relied on a particular API branch may not work the same way after that branch merges; coordinating a breaking change across 4 repos is genuinely difficult.

The hybrid case

Many teams end up in a hybrid: a monorepo with a few services split out for good reasons. The frontend and API are in the monorepo. The auth service is in its own repo because it was acquired. The data pipeline is in its own repo because it has a different release cadence.

Hybrid is fine. Use the polyrepo coordination pattern (deployment manifest) for split-out repos and the monorepo affordances (affected-build computation) within the monorepo. Don’t fight it.

When to consolidate or split

Consolidate to monorepo when you’re frequently making cross-service changes that should be atomic, you’re spending real time on cross-repo coordination, the team is small enough for the build-graph complexity to be manageable, or you want to share types and fixtures more easily.

Split to polyrepo when a service has fundamentally different release cadence, ownership, or compliance requirements, the monorepo build graph is a bottleneck even with good tooling, a service is genuinely independent (a third-party SDK, mobile app, separate product), or teams forming around a service need their own ownership boundary.

Don’t migrate because the other side seems greener. Most pain attributed to repo strategy is actually pain from missing tooling — slow CI, unclear ownership, weak previews — that would be just as painful in the other configuration.

What this means for preview design

Match the tooling to the structure. For monorepos, invest in build-graph tooling and treat previews as full-system deploys with affected services replaced. For polyrepos, invest in cross-repo coordination via manifests and treat each preview as the composition of multiple service refs. Hybrid setups need both. The biggest mistake is using polyrepo machinery in a monorepo (you lose the cross-service coordination benefit) or using monorepo machinery across polyrepos (you can’t actually do this). See The Preview-Friendly Stack Checklist for the broader set of decisions.

If you don’t want to build the build-graph integration, manifest resolver, and per-stack preview compose generation yourself, PreviewProof handles it. Drop us into a Turborepo, Nx workspace, Bazel monorepo, or polyrepo with manifests and we figure out what to deploy. Won’t make the underlying repo decision for you. Will make either decision cheaper to live with.