There are only few topics that spark as much debate as the repository structure - at least that is the case for me and my boss.
“Monorepo vs. Polyrepo” is the new “Sliders vs. Carousel”, but with significantly higher stakes for the developer experience and the velocity of development.
I’m here to take a stance: You should be using a monorepo. And not just for your company’s microservices — but arguably for almost everything you build.
Is it perfect? No. Is it painful at times? Absolutely. But the alternatives are often silent killers of productivity.
Let me address the elephant in the room — or rather, let’s hear from a skeptical duckveloper who’s never worked with a monorepo before.
A monorepo is simply a single repository that contains multiple projects or packages. Instead of having my-app-frontend, my-app-backend, and my-app-shared as separate repos, you’d have one repo with apps/frontend, apps/backend, and packages/shared folders.
The biggest win? When your frontend needs to use a function from the shared package, you just import it directly. No publishing to npm, no version management, no “which version of the shared package is the frontend using again?”
I hear this a lot, but it’s largely a tooling problem that’s already solved. Modern IDEs let you focus on exactly what you need — most developers only interact with 5-10% of the codebase daily. Use VS Code’s workspace settings, “Exclude from Search”, or just good folder structures to narrow your focus to the apps/my-service directory you actually care about.
But here’s the real benefit: when Service A talks to Service B and something breaks, you can jump directly to both codebases in the same editor. No cloning different repos, no multiple windows, no context switching. You can search across both services simultaneously and trace the entire request lifecycle. What seemed like a size problem becomes a debugging superpower.
What about ownership? In separate repos, if you own the repository, you own the code. But in a monorepo, who owns the shared libraries?
# Platform team owns shared infra
/packages/infra/ @org/platform-team
# UI team owns the component library
/packages/ui-kit/ @org/ui-team
# Backend team owns the API
/apps/api/ @org/backend-leads
Now for the practical questions — the scenarios that come up in actual development.
You could , but then you’re making a change, publishing it, then spending weeks bumping versions across services as teams find time to upgrade. Meanwhile you’re maintaining multiple versions and backporting bug fixes.
Even worse. Submodules are notorious: nested .git directories, manual update commands, detached HEAD states, merge conflicts in .gitmodules. When someone updates the submodule, everyone else needs to manually pull and update.
In a monorepo, you edit the code in packages/shared-utils, run tests for all dependent apps immediately, and merge. Everyone is on the latest version instantly — no publishing, no version management nightmare.
Not at all! You can develop in monorepos with multiple packages, then publish their build artifacts to npm for external users. Internally, they get monorepo benefits (atomic commits, easy refactoring, shared tooling). Externally, users consume them as normal npm packages. Best of both worlds!
And when you need versioning, monorepos work beautifully with tools like Changesets . Developers add a markdown file describing changes, and Changesets handles semantic versioning, changelogs, and coordinated releases automatically.
tip
Did you know? Most modern monorepos use this exact approach.
A monorepo with multiple packages, change tracking and version managment by changesets, then publishing to npm for external users.
web development for the rest of us
85.3K 4.7K MIT JavaScript
The React Framework
136.8K 30.1K MIT JavaScript
The web framework for content-driven websites. ⭐ ️ Star to support our work!
55.1K 3K NOASSERTION TypeScript
The Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.
95.3K 11.1K Apache-2.0 TypeScript
Build smaller, faster, and more secure desktop and mobile applications with a web frontend.
100.5K 3.3K Apache-2.0 Rust
Great question! Modern package managers like pnpm and yarn with workspaces solve this with a clever trick: they use a central store for dependencies and create hard links or symlinks. So if ten packages need React 18.2.0, you only download it once. Each package gets a link to the same files in the central store.
The result? A monorepo with 50 packages might have the same disk footprint as those same 50 packages as separate repos — maybe even less, since you’re not duplicating all those node_modules folders. Install times are also faster because the package manager can deduplicate and cache more effectively.
Even with npm, hoisting (where dependencies are lifted to the root node_modules) reduces duplication significantly. Tools like Turborepo and Nx take this further by sharing build artifacts and caches across packages.
Actually, it’s the opposite. Want to change a core API method’s signature? In a polyrepo, you need a multi-week dance: add the new signature for backwards compatibility, publish, coordinate upgrades, then deprecate the old one. In a monorepo, you update the API and every usage of it in the same PR. The compiler shows you all call sites. You fix them atomically, CI validates everything works, and you merge with confidence.
In a mature monorepo setup, breaking main should be theoretically impossible. CI runs tests across the entire affected graph for every PR. If you modify a shared utility, CI automatically identifies all dependent services and validates them before allowing the merge. Tools like merge queues ensure that even if two PRs pass individually, they’re validated together before landing.
That’s an outdated myth from the pre-2020 era. Modern tools like Turborepo are almost zero-config. Add a simple turbo.json file, run npx turbo build, and you’re done. No complex Webpack configs, no custom CI scripts. Today’s tools learned from years of battle-testing at companies like Vercel and Google, packaging all that complexity into simple, ergonomic interfaces.
Creating a new library is often just mkdir packages/new-lib. Your new package immediately inherits all organizational standards — linting, testing, build configuration. Within minutes, other packages can import from it.
It used to. But tools like Turborepo , Nx , and Bazel changed the game with three key features:
Remote Caching : Your entire team’s development history becomes a shared cache. When your coworker builds a package, artifacts are cached remotely. When you check out the same commit, you download the cached result instantly. What took 20 minutes might now take 30 seconds.
Affected Only : You stop validating code you didn’t touch. Change a file in frontend, and the build system skips backend tests entirely. CI times grow with the size of your changes, not your codebase.
Parallel Builds : Modern tools understand the dependency graph and build independent packages simultaneously, maximizing CPU utilization.
Monorepos are actually the best tool for Domain Driven Design . With tools like Nx’s module boundaries or ESLint rules, you can define strict architectural boundaries. For example, enforce that the payment domain can never import from social-features. These boundaries are enforced at build time rather than relying on organizational discipline.
Here’s the thing: you never know which project will grow. That side project you’re building alone today might need a mobile app next month, or a CLI tool, or a separate admin dashboard. By the time you realize you need multiple packages, you’re already dealing with the pain of splitting things up or managing multiple repos.
Starting with a monorepo costs you nothing — even with a single project inside. You still get the benefits of modern tooling like remote caching and fast builds. But you’ve already laid the foundation. When you need to add that second app or extract a shared library, the infrastructure is ready and waiting. No migration, no refactoring, no lost productivity.
Think of it like buying a dining table that can extend. Sure, you might only need four seats today, but having the option costs nothing and saves you from buying a whole new table later.
Maybe you’re reading this and thinking: “This all sounds great, but we already have multiple repositories set up. Can’t we just migrate to a monorepo later when we have time?”
Ah, the classic “we’ll do it later” trap! Here’s the thing: if your codebase has already sprawled across multiple repos, migrating becomes exponentially harder. It’s not just moving files around — you’re untangling years of dependency spaghetti, synchronizing build pipelines, and retraining entire teams. It’s a huge effort and a lot of work - no one wants to do it.
That’s why starting with a monorepo from day one is so important. It’s like trying to merge lanes in heavy traffic versus starting in the right lane from the beginning.
While it sounds restrictive, it’s actually a blessing. In polyrepo setups, autonomy leads to fragmentation: five different testing frameworks, incompatible TypeScript versions, configs no one understands. The monorepo’s alignment reduces cognitive overhead through consistency. Once you learn the patterns in one part of the codebase, those patterns apply everywhere. It eliminates decision fatigue, and the friction of alignment pays off in mobility and standardized excellence.
If you’re still skeptical, consider that the world’s most sophisticated engineering organizations figured this out decades ago:
Google : Their monorepo contains billions of lines of code — they literally built Bazel to make it work at that scale.
Meta : Runs tens of thousands of developers in a single repository.
Uber : Migrated from thousands of polyrepos specifically to solve dependency hell, with documented improvements in their first year.
These aren’t just tech giants with unlimited resources. Projects like Vercel, Svelte, Supabase, and countless others use monorepos because the benefits are real and the tooling is accessible.
Here’s something I strongly believe: everyone should create their own monorepo template.
I’m talking about a personal starter with your go-to stack: your preferred UI framework, a website setup, a docs site, a simple service runtime, and all the bells and whistles — CI/CD pipelines, linting, formatting, the works. Pre-configure everything exactly how you like it.
Yes, it’s a time investment. A significant one. You’ll spend hours (maybe days) getting your tsconfig just right, setting up your build pipelines, configuring your tooling. But here’s the payoff: you do it once .
After that? You never waste time configuring projects from scratch ever again . Every new idea, side project, or client work starts from a battle-tested foundation. No more “should I use ESLint flat config or the old one?” No more “which TypeScript settings do I need again?” No more copy-pasting CI workflows and fixing the paths.
Clone your template, run the setup script, and start building. The infrastructure is already there, already working, already optimized. You go from idea to first commit in minutes instead of hours.
The tooling is mature. The patterns are proven. The investment is minimal, but the payoff is enormous.
“Good programmers know what to write. Great ones know what to rewrite and reuse.”
— Eric S. Raymond