---
title: "Monorepo vs polyrepo: why I kept 5 separate repos"
description: "Monorepo vs polyrepo, from a real project: five repositories (Python, TypeScript, Rust) behind a build web app, what settled it, and what a polyrepo costs."
date: "2026-10-29T13:59:26.431Z"
updated: "2026-10-29T14:24:25.338Z"
locale: "en"
canonical: "https://ninhache.fr/en/blog/monorepo-vs-polyrepo-five-separate-repos"
author: "Néo Almeida"
tags: "architecture, wakfuli"
---

# Monorepo vs polyrepo: why I kept 5 separate repos

Monorepo vs polyrepo: the question comes up as soon as a project splits into several services. To help a player compare two equipment builds, I needed clean, complete, up-to-date game data, resynced with every patch. That prerequisite produced five repositories, and I kept them out of a monorepo.

These five distinct systems share neither a language, nor a pace, nor a load profile. This article covers what settled the monorepo vs polyrepo choice, and what that choice costs me day to day.

Here's the pipeline at a glance, from the game client to the browser:

_A version number flows in a single direction, from the game to the browser._

Each block has its own article in the series; the full tour walks through them one by one:

[The full ecosystem: from a live game to a web app](/en/blog/complete-ecosystem-from-a-live-game-to-a-build)

## The usual monorepo vs polyrepo arguments

Most monorepo vs polyrepo comparisons are written for teams. A monorepo puts the code of multiple projects in one codebase: atomic changes across projects, shared libraries, one copy of each dependency, build tools that understand the whole graph. That's why large companies run monorepos at scale, with dedicated tooling to manage them. Polyrepos give each team its own repository: independent releases, independent permissions, less coordination, and every project picks its own tools.

| | Monorepo | Polyrepo |
|---|---|---|
| Changes across projects | one atomic commit | one release per repo, in order |
| Dependencies | one copy for the whole codebase | each project manages its own |
| Build | needs dedicated tools (Nx, Turborepo, Bazel) to scale | one build per repo, simple but repeated |
| Access control | one repository, per-folder rules at best | permissions per repository |
| Teams | every team works in the same codebase | each team ships independently |

Neither approach is easier in general. The right answer depends on what the projects actually share. In my case, the question wasn't how several teams share code, but how five codebases with nothing in common share data.

## Monorepo vs polyrepo: what settled it

The early temptation would have been a monorepo: one git repository, one history, it's the same project after all. But side by side, these five repositories have almost nothing in common:

| Repository | Language | Runs when | Profile |
|---|---|---|---|
| Decoder | Python | on every game patch | occasional |
| ETL hub | TypeScript | whenever new artifacts ship | publishes versioned JSON and a Postgres database |
| Sprite renderer | Rust | continuously | CPU-heavy, little persistent memory |
| Web app | TypeScript (Next.js, AdonisJS) | continuously | live users, Postgres and Redis |
| Image CDN | TypeScript | continuously | resizing and caching, 90 lines |

Each project has a different rate of change, language, and load profile. The decoder only runs when a patch ships. The renderer, the web app, and the CDN are services that run continuously, and the renderer does it on very little memory. The product moves at the pace of a product with live users. Putting them in a monorepo would have coupled their lifecycles, builds, and deployments for no good reason. Monorepos shine when multiple teams share code and dependencies; here, the only real link from one repo to the next is data.

Then there's who can read what. The decoder is the most sensitive code in the project, and the web app welcomes external contributors. With polyrepos, access control comes for free, since each repository has its own permissions: a contributor to the product never sees the code that reads the game client. In a monorepo, you have to rebuild that isolation by hand, folder by folder.

## The contract between repositories: a one-way version

What makes these five repos cohere isn't a shared deployment or a shared language (there's Python, Rust, TypeScript, SQL, and on the other side the client's decompiled Java), it's a version number that propagates in a single direction.

A patch ships, the decoder produces versioned artifacts, the ETL hub publishes its aggregate, the renderer and the web app each pull the artifacts that concern them, then the product database is resynced.

At no point does a downstream repository guess at or recompute a piece of data

 that an upstream one should have already handed it fully resolved. Each project only knows its neighbors through the versioned format they publish. A monorepo would have made that rule optional: with all the code in one place, nothing stops a project from reaching into another.

## What a polyrepo costs

A polyrepo isn't free. The most concrete cost: changes that span two repos don't fit in one commit. If I add a field in the hub and the web app needs to display it, I first publish a new hub release, then update the product separately. In a monorepo, that would be one commit, one review. In a team, it also means coordinating people; here, it's a matter of release order. Across five projects, that development time adds up.

On top of that, maintenance times five: five configurations, five sets of dependencies to keep up to date, five build pipelines. Monorepos solve this with tooling: Nx or Turborepo share the build setup and only rebuild the code that changed.

**Monorepo**

A cross-cutting edit fits in one commit, and the tooling is shared. But in a monorepo, the five projects also share the history, the access permissions, and the temptation to import a neighbor's code directly.

**Polyrepo (chosen)**

Each repository keeps its language, its pace, and its permissions. Changes across two repos cost two releases, and the interface between projects stays an explicit versioned format.

That cost is the price of an explicit boundary: to touch two projects, you have to go through the versioned format that links them.

**Lesson**

Separating these repositories forced a useful discipline: every boundary is an explicit interface, with a versioned data format, that can't be worked around by quietly importing an internal function from another module.

**Monorepo vs polyrepo in 4 points**

- Polyrepos make sense when projects differ in language, rate of change, and load profile; a monorepo pays off when teams share code.
- Each repository brings its own access permissions, with no isolation to rebuild.
- The price: changes across repositories happen in two steps, release upstream, then update downstream.
- The contract between repositories is a versioned format that only flows one way, from the patch to the product.

This discipline, applied to data whose format and rate of change you don't control, is the thread running through everything else.

[Designing Around Data You Don't Own](/en/blog/designing-around-data-you-dont-own)
