Monorepo vs multirepo: weigh releases, CI, and ownership
Monorepo vs multirepo shapes releases, CI spend, and code ownership. Use this comparison to decide when shared libraries should stay together.

Why this choice gets expensive later
Early on, repo structure feels like a minor setup choice. A few months later, it starts shaping release speed, reviews, and bug fixes.
The pain usually starts around shared libraries. One small change in a common package can force several teams to stop, retest, and line up their releases. What looked like a quick fix turns into approvals, rebuilds, and timing problems.
One repo often feels great at first. You can update a library and every app that uses it in one pull request. No package publishing. No version chase. For a small team moving fast, that convenience is real.
Later, the same setup can drag. More people touch the same codebase, review queues grow, and one merge wakes up CI jobs that only one team needed. Unrelated changes start landing together, so every release carries more risk than it should.
Many repos reduce a different kind of pain. Teams bump into each other less. Access rules are easier to separate. Each codebase can move on its own schedule. The trade is steady admin work: versioning, release notes, dependency updates, publishing, rollbacks, and separate CI rules. None of these jobs looks huge by itself. Together, they can burn hours every week.
That is why this choice is not really about style. It is a trade between shared speed now and coordination cost later. Ignore that trade, and the repo layout starts making decisions for you.
This pattern shows up a lot in growing startups. One repo keeps things simple until three or four teams depend on the same build and test path. Then a change to shared code stops feeling local. Everyone waits behind the same pipeline.
The expensive part is not the repo itself. It is the release habit the repo creates.
What a single repo changes
One repo makes coordinated change easy. If the web app needs a new field and a shared validation library needs to change too, one branch can carry both edits. You do not have to publish a package first or wonder which version is safe to pull in.
It also gives teams one place to inspect shared code. The current API, tests, and examples live together. For a small product team, that clarity removes a lot of daily noise.
The downside shows up fast in CI. A tiny edit in a shared folder can wake a large test run. One line in a utility might trigger builds for the web app, backend, admin panel, and internal tools. Good caching and smarter pipeline rules help, but they do not change the basic trade. One repo can turn a cheap edit into an expensive pipeline.
Ownership can blur too. When everyone can touch shared code, everyone eventually does. That feels efficient until nobody feels responsible for review standards, breaking changes, or cleanup. Shared libraries without clear owners usually collect quick fixes and awkward abstractions that stay far too long.
Imagine a team updating billing screens while changing a shared money-formatting package. In one repo, they can ship both edits together and keep the app working. But if that package also affects several other services, the same pull request may drag in extra reviewers and fill the CI queue.
One repo works best when teams need coordinated changes often, shared code changes quickly, and each shared area has a clear owner. Without those rules, the convenience fades.
What many repos change
Many repos give teams more room to move. One service can ship today while another waits until next week. A payments team does not need to wait for an admin dashboard release, and a mobile team can stay focused on its own deadlines.
CI often gets lighter too. A small change in one repo usually means a smaller test run, a shorter build, and less noise for people who do not care about that change. Once a product grows into several parts with different release rhythms, that can save real money and a lot of waiting.
The trade appears the moment code is shared across repos. Common libraries do not update themselves. Someone has to cut a new version, write a short note, publish it, and make sure each repo pulls it in. That work is manageable once. It gets tiring when it happens every week.
Cross-repo changes also slow in a very specific way. In one repo, a single pull request can update an API, the shared package, and the app that depends on it. In many repos, the same fix may need several pull requests, several CI runs, and a careful release order. If one repo lags, the whole change sits half-finished.
You feel that extra coordination most when shared libraries change often, several teams touch the same workflow, one breaking change needs updates in multiple apps, and nobody clearly owns versioning. Many repos fit best when teams are truly separate, shared code changes slowly, and each repo can live with clear version boundaries.
A practical test is to look at your last three product changes. If most of them stayed inside one area, many repos may fit well. If each change jumped across services, packages, and apps, the neat split on paper may create more admin work than it removes.
How release coupling shows up
Release coupling begins when separate codebases stop moving on their own schedule. It often starts with something small: a shared type, a common client, or a package that every app imports. The code looks tidy. The release plan gets harder.
Shared types and interfaces create quiet ties between teams. Add one required field to a user object, and the web app, admin panel, and background worker may all need changes before anyone can ship with confidence. One library update turns into several coordinated updates.
Bug fixes can cause the same lockstep. A shared auth package might have a serious bug. The fix itself may take an hour. Rolling it out can take a week if four teams need to pull a new version, run tests, and squeeze the update into their own sprint. The fix exists, but the product does not move until the slowest team catches up.
Pinned versions make this worse over time. They protect teams from surprise breakage, which is fair. They also put teams on different timelines. One app stays on 2.3 because it has a launch tomorrow. Another jumps to 2.5 for a feature it needs now. A third skips two releases and hopes to deal with the upgrade later. Before long, nobody talks about "the shared library" as one thing. They talk about which version each app still uses.
If you are trying to spot release coupling, ask a few blunt questions:
- Which team has to wait?
- Who does the upgrade work?
- Can one team release alone?
- How many apps must retest?
- Who decides when old versions stop getting support?
If one library change forces several teams to stop and coordinate, you have release coupling even if the repos are separate. Repo count does not remove the dependency. It only changes where the pain shows up.
That is why ownership matters as much as tooling. If nobody owns migrations, old versions pile up. If one team owns shared code but other teams depend on it every week, those teams can end up blocked by choices they did not make. That tension is usually the first sign that the split is costing more than it saves.
How CI cost grows in real life
CI costs rarely explode on day one. They creep up as the product grows, the test suite expands, and more people push changes every day.
In a monorepo, tiny edits can wake a huge pipeline. A small change in a shared library can trigger builds, tests, and previews across the product, even when most of the code did not change. If path filters are weak or affected-only test logic is missing, runner minutes disappear fast.
A multirepo setup creates a different bill. Each repo often gets its own pipeline file, base image, secrets, runners, lint rules, and security checks. That looks clean at first, but the same setup starts to repeat. Five small repos can cost more than one tuned repo if each one installs the same tools and runs the same scans.
Caching changes the math. So do selective tests. If CI reuses dependencies, skips unchanged packages, and runs only the tests touched by a change, one repo can stay fairly cheap. If every job starts from zero, even a modest codebase feels slow and expensive.
Most teams also undercount the human cost. Cloud spend is easy to see. Engineer time is not. Waiting for checks, fixing flaky jobs, updating copied CI config, and rebuilding the same artifact in several repos all count. Slow pipelines change behavior too. People bundle larger changes, reviewers get slower feedback, and bugs travel further before anyone catches them.
This is where the choice becomes practical. One repo can waste money through overly broad test runs. Many repos can waste money through repeated setup. Teams make better decisions when they measure both: the cloud bill and the hours lost to waiting. If you only track the invoice, you miss the bigger cost.
Who should own shared code
Shared libraries age badly when everyone can edit them and nobody owns the fallout. Repo structure does not solve that. One repo can still hide messy ownership, and many repos can work fine if people know who decides what.
Give each shared library one clearly named owner group. Keep it small. Two or three maintainers usually work better than a committee. They should decide which changes go in, when to cut a release, and how long older behavior stays supported.
The best owners usually sit close to the teams that use the library every week. If a platform team owns code that product teams depend on, those product teams need a real voice. Distance leads to bad calls. Maintainers stop feeling the pain of a breaking change, and users start working around the library instead of improving it.
A simple split works well: the team with the deepest knowledge owns the internals, and the teams with the heaviest use help shape the public interface.
Set review rules early, before small changes turn into a long queue and surprise breakages become normal. Keep the rules light:
- Library owners approve public API changes.
- At least one consuming team reviews breaking or risky changes.
- Each release gets a short change note, and a migration note when needed.
- Tests cover the behavior other teams rely on.
Surprises are what people remember. If a library update quietly changes a return value, naming rule, or config format, other teams lose time hunting the problem. It is usually better to deprecate first, keep the old path working for a short period, and remove it on a known date.
A small example makes this concrete. If checkout, billing, and reporting all use the same pricing library, billing might own the logic, but checkout and reporting should review any change to the public methods they call. That catches breakage early and keeps the library useful instead of feared.
How to decide step by step
Start with the code you already share, not with repo ideology. Teams often turn this into a theory fight, then split too early or stay together too long.
A better approach is boring, and that is why it works: map what exists, watch what changes, and measure the work your team pays for every week.
- List every product, service, and app that uses the same code. Include internal tools if they depend on the same libraries. You want a plain map of real dependencies, not guesses.
- Mark the libraries that change all the time. A package that moves every week creates daily coordination work. A library that barely changes is a weaker reason to split.
- Check release timing by team, not by architecture diagram. If one team ships twice a day and another ships once a month, tight coupling gets expensive fast.
- Compare CI cost with the cost of package management and extra reviews. One repo can make every pull request run too much. Many repos reduce some of that waste, but they add version bumps, dependency updates, and more release steps. Count hours, not just build minutes.
- Test a small split before you reorganize everything. Pick one shared library with clear owners and stable boundaries. Move only that part, then watch release speed, broken builds, review delays, and support questions for a few weeks.
This works better than a big cleanup project because it gives you evidence. Oleg Sotnikov often takes this approach in Fractional CTO work: start with one contained change, measure the effect, then decide whether the broader repo structure still makes sense.
If your pilot split creates more versioning work than it saves, keep the repo together a bit longer. If it reduces release blocking and cuts review noise, you have a solid reason to keep going.
A simple example
Picture a small SaaS team with a customer web app, an admin panel for support, and a background worker that handles billing emails and sync jobs. They also maintain two shared libraries: one for auth rules and one for UI components.
Early on, one repo feels great. A developer can change the auth library, update the login screen, fix the admin panel, and adjust the worker in one branch. One review covers the whole change. One CI run shows whether the refactor broke anything. For a young product, that speed is hard to beat.
This is why a monorepo often feels like the easy choice at first. If the same people own everything and they usually ship together, one repo removes friction.
The pressure appears later. The company adds a partner API team that needs its own release pace. That team cannot wait for the web app and admin panel every time it wants to ship a small auth change. Now release coupling starts to hurt. A library update is no longer just an internal refactor. It becomes a versioning decision.
Ownership gets messier too. The original product team still wants to move fast, but the partner API team needs stricter rules. Who approves changes in the auth library? Who decides when a breaking change is allowed? In one repo, teams often handle this with tighter ownership rules and narrower CI checks. In many repos, they handle it with package versions and explicit upgrade windows.
Neither choice is automatically better. In this example, one repo worked well while one team shipped one product. Once another team needed a different schedule, versioning and ownership started mattering more than raw refactor speed. That is usually the point where repo shape begins to affect daily work.
Mistakes that lead to the wrong split
Many teams split a repo because it feels messy. That is usually the wrong reason. Messy folders, slow tests, and unclear build scripts are local problems. You can fix them without changing the whole release model.
The opposite mistake happens too. Teams keep one repo long after they stop sharing deadlines. The backend ships daily, the mobile app ships every two weeks, and an internal admin tool changes once a month. If every change still goes through one release flow, people start waiting on work that has nothing to do with them.
Shared libraries create another common failure. A team changes a library API and expects every service to upgrade at once. That can work for a while in one repo. It breaks fast when teams need different upgrade timing. If you split, plan a support window for older versions. If you do not, one library update can turn into a surprise migration project.
Copying library code is a bad escape hatch. Teams do it to avoid ownership arguments, but the bill arrives later. One copy gets a bug fix, another gets a security patch, and a third stays old because nobody remembers it exists. What looked faster becomes harder to track and more expensive to maintain.
Package publishing has its own hidden cost. Every new repo can add version rules, release notes, access control, dependency update chores, broken publish pipelines, and meetings about "which version are we on?" That overhead is real. In repo decisions, teams often count code boundaries and forget the daily maintenance around them.
A good rule is simple: split when release timing, ownership, and version support need real separation. Do not split just because the repo looks untidy. Clean up the code first, then see whether the pain is still there.
Quick checks and next steps
A good repo setup should make daily work easier within a few weeks. If the change you are planning adds more process than speed, pause before you move anything.
Start with reviews. Can one team still review most changes fast enough? If reviewers understand the product, the shared libraries, and the release flow, one repo may still be the cheaper option. If reviews bounce between teams for days, ownership is probably too blurry.
Then look at release dates, not theory. If two products need different release calendars now and one keeps blocking the other, release coupling is already costing you time. That is a real reason to consider separate repos. If they still ship together most of the time, splitting may only add version work.
A short checklist helps:
- Track how long reviews wait before someone starts them.
- Check whether products need separate release calendars now.
- Compare current CI time with the extra work of packaging and versioning shared code.
- Write down who owns each shared library before any move.
CI cost fools teams all the time. A split can reduce test scope in one place, but it can also add package publishing, dependency updates, broken version chains, and more release checks. If you do not count that extra work, the savings on paper are fake.
Before you change structure, write down three things: owners, version rules, and release boundaries. Name the team that approves shared code. Decide when a library gets a new version. State which changes can ship alone and which must ship together. The document does not need to be long, but a new engineer should be able to follow it.
If the tradeoffs still feel muddy, an outside review can help. Oleg Sotnikov at oleg.is does this kind of Fractional CTO advisory work, including practical reviews of release flow, CI overhead, and shared code ownership.
Frequently Asked Questions
How do I know if one repo is still the right choice?
Check your last few product changes. If most changes touched several apps or shared packages and one team could still ship them together without long review delays, one repo still fits.
If review queues keep growing, CI wakes too much work, or one team often waits for another team’s release, the repo structure now costs you time.
When does a monorepo start slowing teams down?
It usually starts when shared code changes stop feeling local. A small library edit pulls in extra reviewers, wakes large CI runs, and makes unrelated teams retest work they did not touch.
You will notice it in daily work before you notice it in architecture diagrams. Merges slow down, releases bunch together, and people avoid small cleanup because the pipeline feels too expensive.
Are many repos always better once the team grows?
No. Many repos help when teams ship on different schedules and shared code changes slowly.
They also add steady work. Someone has to cut versions, publish packages, update dependencies, and keep release order straight. If your teams still change the same workflow every week, many repos can add more admin than relief.
What actually causes release coupling?
Release coupling starts when one change needs several teams to move together. Shared types, common clients, and widely used libraries create that pressure fast.
Repo count does not remove the dependency. One repo shows the pain in CI and reviews, while many repos show it in version bumps, upgrade work, and release timing.
Can I cut CI cost without splitting the repo?
Tune the pipeline before you split the code. Use path rules, cache dependencies, run only affected tests, and stop rebuilding apps that a change did not touch.
Also check human delay, not just runner minutes. If engineers wait on flaky jobs or broad test runs, fix that first. A cleaner pipeline often buys more time than a repo reorg.
Who should own shared libraries?
Give each shared library a small owner group. That group should review API changes, decide release timing, and keep migration notes short and clear.
Do not leave shared code open to everyone with no owner. Teams will patch it quickly, but nobody will clean it up or handle breakage for the teams that depend on it.
Should I split the repo just because it feels messy?
Usually no. Messy folders, weak tests, and confusing build scripts are codebase problems, not repo problems.
Clean those up first. If teams still block each other after that, then the split may solve a real release issue instead of hiding a maintenance issue.
What extra work comes with versioning and package publishing?
Every shared package needs version rules, release notes, publishing, dependency updates, and rollback plans. None of that feels big once, but it adds up when teams do it every week.
That overhead often shows up in engineer time, not just tooling. People spend hours checking compatibility, chasing old versions, and fixing half-finished cross repo changes.
What is the safest way to test a repo split?
Pick one shared library with clear boundaries and move only that part first. Then watch review speed, broken builds, support questions, and how often teams wait on each other.
A small pilot gives you real data. If the split reduces blocking, keep going. If it only adds version work, stop before you reshape everything.
When should we stay with a monorepo?
Keep one repo when the same people own most of the product, teams ship on the same schedule, and shared code changes often. In that setup, coordinated edits in one pull request save real time.
It also works better when each shared area has a clear owner and CI can stay selective. Without those two rules, the convenience fades fast.