Repository layout sounds like the most boring decision in software. It's actually one you live inside every single day: it decides how much of the system you can see at once, how changes flow to production, and how often your tools fight you instead of helping. For a platform made of many small services maintained by one person, the layout question had real stakes. Here's where it landed, and what the trade-offs feel like a year in.
The shape: a workspace of repositories
The top level is a single workspace repository — the superproject — that contains every backend service, both frontends, infrastructure configuration and the cross-cutting documentation. Each deployable service is its own git repository, mounted into the workspace as a submodule. Package management inside is pnpm workspaces, chosen for disk-efficient installs and strictness about undeclared dependencies — phantom imports that "work by accident" in other setups fail fast here, which is exactly the kind of failure I like.
Why not one big repository?
A true monorepo — every service as a folder in one history — was the obvious default, and plenty of teams are happy there. What pushed against it: deploy independence. Each service here ships from its own repository with its own small CI pipeline; merging to a service's main branch builds and deploys that service and nothing else. The blast radius of any change is visible in the URL of the pull request. With a shared history, that isolation is reconstructed with path filters and tooling — workable, but it's machinery you must build and trust. With per-service repos, isolation is the default and integration is the machinery. For a solo maintainer, defaults matter more than ceilings.
Why not fully separate repositories, then?
Because context is the scarcest resource on a team of one. Twenty scattered repos means twenty clones, twenty READMEs drifting independently, and no single place where the whole system is visible. The superproject solves precisely that: one clone gives the complete picture, cross-service refactors happen in one editor window, and system-level documentation — architecture, operations, feature inventory — lives at the root where it can describe the forest instead of one tree. The submodule pointer commits even double as a coarse system changelog: the superproject's history records which services moved, when.
The pragmatic exception
Consistency is a value, not a religion. One corner of the codebase — the serverless functions deployed with a vendor CLI — stays on npm rather than pnpm, because that CLI's packaging quietly assumes npm's node_modules layout. I spent an afternoon trying to force elegance, then wrote the exception down and moved on. A documented inconsistency you understand beats a clever workaround you'll have to re-debug every tooling update. The rule of thumb that came out of it: fight your tools for principles, not for aesthetics.
Documentation lives where the code lives
Every service carries its own operational notes — what it does, what it needs, what its quirks are — and the root carries the system-level docs. The standing rule is that a behaviour-changing pull request updates the relevant docs in the same diff, because documentation updated "later" is documentation that lies. This matters double when much of the day-to-day coding happens with an AI pair — a workflow described in an earlier entry — since the docs are literally the context the assistant reads. Stale docs don't just mislead future-me; they actively steer the tooling wrong today. Keeping them truthful is operationally load-bearing.
A year in: the honest scorecard
What works: deploys are boring, blast radii are small, the whole system fits in one clone, and no dependency sneaks in undeclared. What costs: submodules have sharp edges — pointer updates are a ritual you must not skip, and Windows adds its own seasoning with long paths and file locks. Would I choose it again? Yes — with the caveat that this layout is right for many deployables, one maintainer. Different ratio, different answer. Repository layout is like database choice: the best one is the one whose failure modes you understand and can afford.
A practical note for anyone replicating this on Windows: git worktrees earn their keep hard here. Feature work happens in a worktree checked out from the remote default branch, which keeps long-running local state out of pull requests entirely — and sidesteps the file-locking misery of switching branches under an editor, a package manager and a dev server all at once. The cleanup discipline (prune worktrees, mind the path-length limits) took a few stubbed toes to learn, but parallel isolated checkouts turn out to be the single best ergonomic upgrade a many-repo workspace can buy.