All articles

Monorepo vs polyrepo: why I kept 5 separate repos

6 min read

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:

Game client
opaque binary
Decoder
tracks the drift
ETL hub
aggregates, versions
Web app + rendering
Postgres, Rust
Browser
builds compared
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 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.

MonorepoPolyrepo
Changes across projectsone atomic commitone release per repo, in order
Dependenciesone copy for the whole codebaseeach project manages its own
Buildneeds dedicated tools (Nx, Turborepo, Bazel) to scaleone build per repo, simple but repeated
Access controlone repository, per-folder rules at bestpermissions per repository
Teamsevery team works in the same codebaseeach 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:

RepositoryLanguageRuns whenProfile
DecoderPythonon every game patchoccasional
ETL hubTypeScriptwhenever new artifacts shippublishes versioned JSON and a Postgres database
Sprite rendererRustcontinuouslyCPU-heavy, little persistent memory
Web appTypeScript (Next.js, AdonisJS)continuouslylive users, Postgres and Redis
Image CDNTypeScriptcontinuouslyresizing 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.

Reactions

Click an icon to react.

Comments

No comment · no reply

Add a comment

No discussion yet.
Be the first to leave a comment.

Secrets

0/20. 20 to go.

Finish a tier to make the next one’s hints clear.

Terminal