GitOps Repository Design
GitOps Repository Design
The single most consequential decision you make when adopting GitOps is how you lay out your repositories. Get this wrong and you will fight the tooling every day — drift between environments, unintended promotions, secrets leaking across team boundaries, and CI pipelines that are terrified to merge anything. Get it right and you have a delivery system that scales from one team and one cluster to fifty teams and hundreds of clusters with minimal additional overhead.
This lesson covers the three structural decisions every GitOps organisation must make: app repos versus infra repos, environment folders versus environment branches, and mono-repo versus multi-repo. Each decision has a real cost profile, and the right answer depends on your team topology, your compliance requirements, and your expected rate of change.
App Repos vs Infrastructure Repos
The most fundamental split in GitOps is between the repository that holds application source code and the repository that holds cluster-desired-state manifests. These are different concerns with different change rates, different audiences, and different access controls — keeping them separate is not just convention, it is architectural hygiene.
The app repo (sometimes called the "source repo") contains your Go, Python, TypeScript, or Java code, its Dockerfile, and the CI pipeline definition. Every merge to main triggers a build, runs tests, pushes an image tagged with the Git SHA, and — critically — opens a pull request against the infra repo updating the image tag. The app repo does not deploy anything directly.
The infra repo (the "config repo" or "GitOps repo") contains only Kubernetes manifests, Helm values files, or Kustomize overlays. It has no build artefacts and no application logic. Its single job is to be the source of truth for what should be running in every cluster. ArgoCD or Flux watches this repo and continuously reconciles the cluster to match it.
A concrete CI flow that wires these two repos together looks like this. Your GitHub Actions or GitLab CI workflow in the app repo builds and pushes the image, then uses a PAT or deploy key to open a PR against the infra repo:
The PR is reviewed by an oncall engineer (or automatically merged if you trust your integration tests), and ArgoCD/Flux picks up the merged change within seconds. This two-repo pattern is what Weaveworks, Google, and Lyft document as the canonical GitOps topology.
Environment Folders vs Environment Branches
Once you have an infra repo, you face a second decision: how do you represent multiple environments (dev, staging, production) inside it? Two schools of thought exist, and one of them will cause you pain at scale.
Environment branches — a dedicated Git branch per environment (env/dev, env/staging, env/prod) — seem intuitive because "you promote by merging". In practice, branch-per-environment has catastrophic failure modes. Cherry-picks fail silently. Merge conflicts between env branches accumulate over weeks. You cannot easily diff "what is in staging but not prod" without comparing branch tips across the whole tree. Long-lived divergent branches are exactly what modern Git workflows were designed to eliminate. Do not use environment branches.
Environment folders — a single main branch with directories per environment — is the correct approach. Every change is a regular PR to main, the history is linear and readable, and diffing environments is a file-system diff, not a Git branch diff. Kustomize was purpose-built for this model; ArgoCD ApplicationSets and Flux Kustomization resources are designed to point at directories within a single branch.
A well-structured infra repo layout:
staging/values.yaml to prod/values.yaml. Nothing else changes. This makes the promotion diff a one-liner and the rollback a one-line revert — both auditable in Git history forever.
Mono-Repo vs Multi-Repo
The third axis is whether all your application infra config lives in one repository or is split per team or per service. This is where team topology — Conway's Law — matters most.
Mono-repo (all services in one infra repo) works extremely well for organisations up to roughly twenty to thirty teams. It gives you a single place to enforce policies (OPA/Kyverno policies are checked uniformly), makes cross-service dependency changes atomic (when you upgrade a shared Helm chart version you do it in one PR), and simplifies GitOps operator configuration (ArgoCD points at one repo). Google, Spotify, and Shopify successfully run mono-repos. The operational challenge is access control: teams must not be able to modify each other's directories. Solve this with CODEOWNERS files.
Multi-repo (one infra repo per service or per team) provides hard isolation boundaries and is the natural choice when regulatory compliance requires it (PCI DSS zone isolation, HIPAA data segmentation) or when teams are in separate legal entities. The cost is operator sprawl: every ArgoCD Application or Flux GitRepository object multiplies, cross-service changes need coordinated PRs in multiple repos, and policy enforcement must happen independently in each repo (or via a separate policy repo watched by an admission controller).
A practical rule of thumb: start with a mono-repo split into teams/ subdirectories, enforced by CODEOWNERS. Migrate a team to their own repo only when there is a documented compliance or organisational boundary that makes it mandatory. Here is the CODEOWNERS pattern for mono-repo access control:
With branch protection requiring CODEOWNERS review, a developer on the payment team cannot accidentally — or maliciously — push a change that modifies the checkout service manifests. This single file enforces multi-team isolation without splitting into multiple repos.
Putting It All Together
The canonical big-tech GitOps repository design for a growing engineering organisation is: separate app and infra repos, environment folders on a single branch, and a mono-repo with CODEOWNERS until you hit a concrete reason to split. CI in the app repo promotes by opening PRs against the infra repo. The GitOps operator (ArgoCD or Flux) watches specific paths within the infra repo for each environment, applying changes automatically on merge. Drift detection and rollback become trivial — they are just Git operations on a linear history.