Skip to content
Kumar Chandrachooda
Microservices

Thirteen Repos and a Floating Version

Pacco is thirteen sibling repositories held together by a solution file of relative paths, clone scripts with a dual-branch habit, and 284 package references that all say 0.4.* - coherence by wildcard, with a name-squatted saga library and a README that forgot a repo along the way.

By Kumar Chandrachooda 08 Jan 2026 7 min read
Twelve boxes on one shelf, held by a path convention and an asterisk

Ask what makes Pacco's thirteen repositories one system and the honest answer is: a directory convention, an asterisk, and habit. There is no monorepo, no internal package feed, no estate-level build, no manifest that records which thirteen commits ever worked together. Identity was the estate at its smallest scale — one service, fifteen files; this part is the opposite altitude, the estate as a topology: how twelve code repos and one umbrella are assembled, versioned, updated and (not quite) verified as a whole. Polyrepo is a legitimate choice, and Pacco makes it with real craft in places — but every place the craft runs out, a governance question is standing there unanswered, and those questions are the article.

A solution made of ..\

The umbrella repo, Pacco, contains no code — compose files, scripts, docs, and one remarkable artefact: Pacco.sln, which aggregates 41 csproj files from all twelve sibling repos by relative path:

"..\Pacco.Services.Customers\src\Pacco.Services.Customers.Api\Pacco.Services.Customers.Api.csproj"

A solution-of-solutions, reaching out of its own repository. It only opens if every repo is cloned as a sibling under one parent directory — the filesystem layout is load-bearing, an implicit contract between the .sln, the clone scripts, and (as part 11 will show) the compose files whose build: contexts also reach across ../../. Nothing documents the contract; everything assumes it. For local development it is genuinely pleasant — one solution, the whole estate navigable, go-to-definition across service boundaries — and it costs nothing until the day one repo moves or renames, at which point the failure appears in a different repo than the change did. That displacement — cause here, symptom there — is the polyrepo tax in miniature, and it recurs in every section below.

Assembly by script, coherence by branch

The estate assembles itself with four small scripts in Pacco/scripts/. git-clone.sh loops over a twelve-repo array cloning from github.com/devmentors; git-clone-fast.sh pipes the same array through xargs -P 0 for parallel cloning (a genuinely nice DX touch — the estate is ~13 clones, and someone cared about the wall-clock); git-clone.ps1 gives Windows parity. The interesting one is git-pull.sh:

cd $REPOSITORY && git checkout develop && git pull && git checkout master && git pull && cd ..

Both branches, every repo, every update — because the estate has a dual-branch convention wired all the way to Docker Hub: each repo's dockerize.sh tags master builds as latest (plus the Travis build number) and develop builds as dev (plus dev-N). The updater maintaining both branches is the convention made executable. What the scripts never create is a snapshot: there is no lock, no manifest, no tag that says “these twelve SHAs were tested together”. Estate coherence is “whatever master is in all twelve repos this morning” — a definition that is true right up until two repos' masters disagree about a contract, which the correlation-spine and wrong-exchange stories elsewhere in this series show is not hypothetical.

The README that forgot a repo

Here is a small finding with a big lesson. The umbrella README's “Which repositories should I clone?” section lists twelve repositories. Thirteen exist. The missing one is Pacco.APIGateway.Ocelot — the alternative gateway from part 4 — which is present in the clone scripts' REPOSITORIES array. So the estate has three registries of its own membership — the README, the script, and GitHub reality — and they pairwise disagree. A newcomer following the README never learns the Ocelot gateway exists; a newcomer running the script clones a repo the documentation never mentions and no compose file runs. Neither artefact is wrong by its own lights; they simply answer to no common source. A repo list maintained in two places is not documentation, it is two opinions — and the fix is structural, not editorial: generate the README's list from the script's array (or both from a manifest), so forgetting becomes impossible rather than merely regrettable.

Version 0.4.*, quantity 284

Open any csproj in any service and the dependency block looks like this — from Operations, trimmed:

<PackageReference Include="Convey" Version="0.4.*" />
<PackageReference Include="Convey.Auth" Version="0.4.*" />
<PackageReference Include="Convey.MessageBrokers.RabbitMQ" Version="0.4.*" />
<PackageReference Include="Convey.Persistence.Redis" Version="0.4.*" />

Across the thirteen repos there are 284 package references floating at 0.4.* — every Convey package, every Ntrada package, all of them — and zero lock files of any kind. This is the estate's boldest governance decision, and it deserves to be stated as the trade it is rather than sneered at. The wildcard buys real things: thirteen repos stay on the same framework minor without a thirteen-PR version-bump ceremony every release, and a Convey fix propagates to the whole estate on the next restore, no coordination required. All repos drift, but they drift together — coherence by wildcard.

Now the invoice. First, restore time became a coordinate of your build: two developers restoring a week apart can compile different dependency graphs from identical source, and without lock files nothing records which graph either of them got. Second, historical builds are unreproducible — check out last year's commit and you will restore today's newest 0.4.x, so the past cannot be rebuilt as it was. Third, the estate has delegated its release cadence to an upstream it does not control: a bad 0.4.x ships into all thirteen repos simultaneously, with no commit anywhere in the estate to revert. The wildcard is a floating foreign-exchange position, and the missing instrument is the same one the branch section missed — a periodic snapshot (“this resolved set works”) that turns drift together into advance together, deliberately. What the same wildcard does to the estate's dependency hygiene at the service level — packages registered, never used, drifting in step — is the graveyard of registered dependencies' story.

The package with the underscore

One dependency in the estate is pinned, and its name is the story: OrderMaker's csproj references

<PackageReference Include="Chronicle_" Version="3.2.1" />

Chronicle_, trailing underscore, version 3.2.1 exactly. The original Chronicle package id was squatted on NuGet, so the library's authors republished under the nearest available name — a name-squat workaround immortalised in every consumer's csproj. It is easy to read as trivia; it is actually the purest possible demonstration that package identity is a namespace you do not own. The public id your architecture depends on is allocated first-come on a registry you do not control, and the mitigation (reserved prefixes, or an internal feed proxying the public one) is a governance control almost nobody installs before the day they need it. The underscore is also, quietly, the one place the estate pins a version — the library that coordinates a distributed saga is the one dependency nobody was willing to let float. The instinct was right; it just never generalised.

Twelve pipelines, no estate

CI completes the pattern. Twelve repos carry a byte-similar .travis.yml — xenial, dotnet 3.1.100, master and develop only — running build.sh, test.sh, then dockerize.sh to push devmentors/* images to Docker Hub. Twelve independent pipelines; no estate-level build anywhere. Nothing ever compiles the thirteen repos together, runs a cross-service test, or so much as boots the compose stack and curls /ping — the only cross-repo verification in the entire estate is the Orders↔Parcels Pact pair, and each half runs inside its own repo's pipeline. And the one repo with no CI at all is the umbrella — the repo whose solution file, scripts and compose stack define what “the estate” even means is the only one no machine ever checks. (The Travis badges are dead today, netcoreapp3.1 being a 2020-era estate; the archaeology is part of the honesty.)

The polyrepo scorecard, then. Pacco's mechanics are craftsmanlike — parallel clone scripts, a dual-branch convention wired to image tags, a solution file that makes thirteen repos feel like one codebase. What is missing is everything that would make the estate a system of record: a lock (which resolved packages), a snapshot (which SHAs), a registry (which repos), an integration gate (do they still compose). My rule of thumb from reading it: a polyrepo is a distributed system made of repositories, and it needs what you demand of any distributed system — versioned contracts, a health check for the whole, and one source of truth for membership. Pacco has services that meet that bar and a repo topology that doesn't, which makes it the rare estate where the architecture is more disciplined than the meta-architecture.

The umbrella has a second half I have kept deferring: not how the estate is assembled, but how it runs — seven compose files in careful layers, PM2 manifests that quietly omit the saga service, a 400-line run-book, and a .rest file that is the best onboarding document in the whole system. Next: the repo that runs the other twelve.