Moon Pixels Limited project availability
Back to articles

Monorepo vs polyrepo: when a monorepo makes sense for tightly coupled apps

11 min read

One feature. Three pull requests. Three releases. Same engineer.

That was the point where “this is a sensible separation of concerns” started to look a lot like “we have made routine work annoyingly ceremonial”. We had a backend API, a consumer-facing frontend, and an internal staff frontend. They lived in separate repositories, but most meaningful features touched all three anyway.

That is the smell this post is really about. Not “monorepos are trendy now”, and not “every team should collapse everything into one giant repo”. I mean the more useful question: when your apps are tightly coupled in practice, when does a monorepo make more sense than a polyrepo?

My answer is simple. If the same full-stack engineer is regularly changing all of them, and you are releasing them together anyway, repo boundaries may be paperwork rather than architecture.

The real question is not monorepo vs polyrepo, it is coupling

There is plenty of reasonable guidance defending both approaches. GitHub’s own architecture guidance on polyrepos is aimed at autonomy, clearer ownership, and separate release cadences across components. It also explicitly says that tightly coupled components may find a monorepo simpler, while a meta-repo style of coordination adds overhead that is more useful at larger scales and looser boundaries (GitHub Well-Architected on polyrepo engineering).

That lines up with my experience.

The common monorepo pitch is usually about shared packages, shared lint rules, shared UI components, and clever workspace tooling. All useful. But I think that framing misses the more important signal.

The stronger signal is this:

  • the apps ship on the same release train,
  • the same features cross the same boundaries every week,
  • the same engineers work across all of them,
  • and the cost of coordinating repos is higher than the value of separating them.

That is what happened here. We did not move to a monorepo because we were desperate to share a button component. We moved because the three codebases already behaved like one system.

The smell was three repos pretending to be independent

Before the migration, the layout looked more or less like this:

driver-portal-api/
└── …

driver-portal-frontend/
└── …

driver-portal-staff/
└── …

On paper, that looks tidy. In practice, a normal end-to-end feature often meant:

  1. change the API,
  2. update the consumer frontend,
  3. update the staff frontend,
  4. raise three PRs,
  5. get them reviewed in sync,
  6. release three apps without missing one.

None of that is impossible. It is just expensive in a very unglamorous way.

The awkward bit was that the boundary was not a team boundary, and it was not really a language boundary either. The engineers working on the feature were full-stack. The same person was often making all three changes. So the supposed separation was not protecting a distinct workflow. It was mostly adding coordination overhead.

That is why I think “monorepo vs polyrepo” is often framed too abstractly. The practical question is: are these apps actually independent, or are they just stored independently?

What changed when we moved to one repo

After the migration, the structure became this:

driver-portal/
├── backend/
│   └── …
├── frontend/
│   └── …
├── staff/
│   └── …
├── .github/
│   └── …
└── …

The obvious benefit was operational: we went from potentially three PRs and three releases for an end-to-end feature to one PR and one coordinated release process.

That does not sound especially revolutionary, but it’s still one of the best improvements we made.

The change also made CI/CD much more coherent. GitHub Actions supports both path filters and reusable workflows, which is exactly what you want in a monorepo. You can centralise tests, checks, and release logic, while still deciding which jobs should react to which paths. The important word there is “can”. You do have to revisit those assumptions once apps move into nested directories.

From a developer experience point of view, the gain was simpler still: engineers could work on a feature from API through to UI in one place, review it in one PR, and release it through one pipeline.

That matters. It reduces the admin around the work, which means you get to spend more of your brain on the work itself.

A monorepo is only better if you do the boring bits properly

I do not want to pretend the migration was magically effortless because that would be a bit dishonest.

The awkward parts were mostly the boring operational ones.

Deployment tooling had assumptions baked into it

The old setup assumed each app lived at the repo root. Once everything moved under backend/, frontend/, and staff/, those assumptions stopped being true.

Build commands, deployment paths, and workflow triggers all needed another look. GitHub’s monorepo guidance calls this out directly: repo structure and release cadence should shape the CI/CD design, not the other way round (GitHub Well-Architected on monorepos).

That was exactly the job here. The repo move itself was straightforward enough. The real work was teaching the surrounding tooling that the code had moved house.

We needed a hard cutover, not a vague transition

The other important constraint was making sure engineers stopped working in the old repos at the right moment.

This is the bit people underplay. You can do the technical migration perfectly and still create a mess if two codebases stay half-alive for too long. That is how changes get missed, cherry-picked badly, or quietly reintroduced in the wrong place.

So we treated cutover as a real engineering event, not a footnote. There needed to be a clean point where the old repos stopped being the source of truth and the new monorepo became it.

That was not glamorous work either. It was still crucial, though.

The tooling case for monorepos is broader than shared packages

One useful detail from the wider monorepo ecosystem is that modern tooling is very comfortable with heterogeneous codebases.

pnpm workspaces are built specifically to manage multiple projects in one repository. Turborepo package configurations let packages inherit root behaviour while still overriding task config per app. And Nx makes a point I think is worth repeating: a monorepo is not just code collocation, it is the combination of shared repo plus tooling and boundaries that make coordinated work practical (Nx monorepo concepts).

That matters because one of the lazier objections to monorepos is that they force everything into one mould. Good monorepos do not do that. They standardise where standardisation helps, and allow local differences where it does not.

In our case, the value was not “every app is now identical”. It was “every app can now participate in one coherent workflow”. Different apps still had their own concerns. They were just no longer pretending to be unrelated systems.

AI changes the trade-offs a bit, even if it is not the whole story

I would not choose a repo strategy purely for AI tooling. That would be daft.

I do think AI has changed the trade-offs enough to be worth mentioning.

One frustration with separate repos is that cross-cutting work becomes harder for agents for the same reason it becomes harder for humans: the context is fragmented. GitHub Copilot CLI now explicitly documents working across multiple repositories either from a parent directory or by adding more directories to the session (GitHub Copilot CLI best practices). Polyscope has linked workspaces so one agent can reference another workspace’s context across repositories while keeping isolated clones.

That is useful, and it proves the problem is real. Tool vendors are not building these features for the exercise.

But if your apps are tightly coupled and already released together, a real monorepo still has the simpler mental model. The agent can see the system in one place, just like the engineer can. No added coordination layer, no stitched-together context, no explaining which repo owns which half of the feature this week.

That does not make polyrepos obsolete. It just means that in the age of agentic tooling, shared context now has practical value, not merely philosophical value.

A tiny OpenCode example from this migration

One thing I wanted to avoid losing was the ability to work at the child app level when needed, while still giving root-level tooling access to project skills.

This is the small OpenCode plugin I used to load skill directories from the child apps into the root repo context:

import fs from 'node:fs';
import path from 'node:path';

const childSkillDirectories = ['backend', 'frontend', 'staff'].map((app) =>
  path.join(app, '.agents', 'skills'),
);

const getChildSkillsPaths = (directory) =>
  childSkillDirectories
    .map((relativePath) => path.join(directory, relativePath))
    .filter((skillsPath) => fs.existsSync(skillsPath));

const addSkillsPath = (config, skillsPath) => {
  config.skills = config.skills ?? {};
  config.skills.paths = config.skills.paths ?? [];

  if (!config.skills.paths.includes(skillsPath)) {
    config.skills.paths.push(skillsPath);
  }
};

export const DriverPortalRootSkillsPlugin = async ({ directory }) => ({
  config: async (config) => {
    for (const skillsPath of getChildSkillsPaths(directory)) {
      addSkillsPath(config, skillsPath);
    }
  },
});

I like this pattern because it reflects the real compromise.

Each child project still owns its own skills directory, so the backend, consumer frontend, and staff frontend can evolve those skills independently. If a skill is added to one app, removed from another, or reorganised locally, the root setup does not need a parallel manual update. The plugin simply checks which child skill paths exist and adds the available ones into the root config.

That meant we did not have to flatten all agent knowledge into one shared directory just because the code moved into one repo. Teams could keep the app-specific skills close to the app they belonged to, while the top-level repo still had access to all of them when working across the system end to end.

In other words, the repo became one place to work on features, releases, and automation, without losing the ability to maintain AI context at the project level. That is the bit I cared about most. The monorepo gave us shared context when we wanted it, but it did not force us to abandon isolated context where that still made sense.

When I would still choose a polyrepo

This is the part monorepo advocates sometimes rush past.

I would absolutely still choose separate repos when the systems are genuinely separate.

For example:

  • different teams own them,
  • they release on different cadences,
  • they have distinct risk or compliance boundaries,
  • one system can change significantly without the others caring,
  • or a shared repo would create more noise than value.

That is exactly the case GitHub’s polyrepo guidance is aimed at: autonomy, clear boundaries, and coordination patterns that scale when the repos are truly independent (GitHub Well-Architected on polyrepo engineering).

If your applications are only loosely related, keep them separate. A monorepo is not a badge of maturity. It is just a storage and workflow choice.

But that cuts both ways. A polyrepo is not automatically mature either. Sometimes it is just inertia with extra tabs open.

A simple smell test

If you are unsure, this is the question I would ask:

Are these apps truly independent, or are we paying a coordination tax to preserve a boundary that no longer reflects how the system is built and shipped?

If most end-to-end features touch all of them, if the same engineers regularly cross the boundaries, and if releases already need to be orchestrated together, that is a strong sign a monorepo may be the more honest model.

Not because monorepos are fashionable. Because the codebase should reflect the system you actually have.

Wrap-up

For this client project, moving from three repos to one monorepo took us from three PRs and three releases to one coordinated workflow. That alone saved time. More importantly, it matched the reality of the work: tightly coupled apps, full-stack engineers, and end-to-end features that were already one piece of engineering whether Git agreed or not.

That is my real view on monorepo vs polyrepo. If the apps are genuinely separate, keep them separate. If they are tightly coupled and move together, a monorepo often makes far more sense than people admit. In 2026, with AI tools making whole-system context even more useful, that case is only getting stronger.

© 2026 Moon Pixels Ltd. (Registered in England and Wales)
Company No. 14080344 VAT No. GB511878679
Privacy Policy
Made in Wales