Skip to main content
Cloud DevOps Solutions
Get a quote Talk to an engineer

GitOps / Helm / ArgoCD

GitOps Repository Structure: App-of-Apps or Monorepo

A GitOps repository structure decides where manifests live, who can change them and how environments relate. We design it around your teams: usually a separate config repository, one folder per cluster and environment, and either app-of-apps or a monorepo root that fans out to applications.

Key takeaways

  • Application code and deployment configuration usually belong in separate repositories with separate review rules.
  • Structure by environment and cluster in folders, not in long-lived branches.
  • App-of-apps and monorepo are not rivals: most estates use a root that points to many apps.
  • Ownership boundaries in the repository should match team boundaries, because CODEOWNERS and permissions follow folders.
  • A layout that works for five services often breaks at fifty, so we design for the next stage.

Why repository layout is the real GitOps design decision

Installing a GitOps controller takes an afternoon. Deciding what the repository looks like takes thought, and it is the choice you will live with longest. A bad layout produces merge conflicts between teams, unclear ownership, and promotion that means copying files by hand. A good one makes a new service a ten-line pull request.

This is part of our GitOps, Helm, ArgoCD and Flux service. It follows from installing the controller and feeds directly into environment promotion.

What good looks like

  • Every deployed object traces to a path in git and a reviewer who approved it.
  • A person new to the team can find where a service is configured in under a minute.
  • Teams can change their own services without being able to change someone else’s or the platform’s.
  • Adding a cluster or environment is mostly copying a folder and editing a few values.
  • Nothing important lives only in the cluster or only in a pipeline variable.

The two questions to answer first

Config separate from code?

For most teams we recommend a dedicated config repository. The application repository builds and tests an image. The config repository says which image runs where. This avoids a common trap: a build pipeline that commits a new tag back into the same repository that triggered it, creating loops and noisy history.

The exception is a tiny team with one or two services, where keeping a deploy/ folder beside the code is simpler. Revisit that choice when a third team or a second cluster appears.

Mono or multi repository?

ApproachStrengthsWeaknesses
One config monorepoEasy search and refactoring, single place for standards, simple bootstrapPermissions are coarse, noisy history, large repos can slow some controllers
Platform repo plus per-team reposStrong team isolation, clean permissionsMore repositories to bootstrap, harder cross-cutting changes
Repo per serviceMaximum autonomyHard to see the whole estate, easy for standards to diverge

Most scale-ups settle on the middle option: a platform repository owned by the platform team for ingress, observability and policies, and one or more application repositories for service teams.

How it works in practice

A layout we commonly start from

fleet-config/
  clusters/
    prod-eu/
      root.yaml              # points at apps and platform for this cluster
    staging/
      root.yaml
  platform/
    base/                    # ingress, cert-manager, monitoring
    overlays/
      prod-eu/
      staging/
  apps/
    payments/
      base/                  # deployment, service, config
      overlays/
        staging/
        prod/
    checkout/
      base/
      overlays/

Each cluster folder holds a single root object. That root is the entry point the controller watches, and everything else hangs off it.

App-of-apps in ArgoCD

The root is a parent Application whose source path contains child Application manifests:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root-prod-eu
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/acme/fleet-config.git
    targetRevision: main
    path: clusters/prod-eu/apps
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

Bootstrapping a new cluster then means applying this one object. For larger estates, generating the children is cleaner than hand-writing them, which we cover in ApplicationSets and multi-cluster deployments.

The Flux equivalent

Flux uses a chain of Kustomization objects. A root Kustomization syncs clusters/prod-eu, which contains further Kustomizations for platform and apps, each with dependsOn so that CRDs and add-ons are ready before applications arrive.

Ownership and guardrails

Repository permissions and CODEOWNERS are only useful if folders map to teams. We set rules such as: the payments team owns apps/payments, the platform team owns platform/ and clusters/, and production overlays require a second approver. Combine this with GitOps RBAC so the controller enforces the same boundaries the repository does.

Pull request checks catch broken manifests before merge: kustomize build for every overlay, kubeconform against the Kubernetes schemas, and a policy check for things like missing resource limits or privileged containers.

Decisions and trade-offs

Rendered manifests or templates in git. Storing rendered YAML makes every change visible in a diff but adds a render step. Storing Helm values and Kustomize overlays keeps git small but makes the final output less obvious. We usually keep sources in git and let CI render a preview into the pull request.

Generated versus hand-written Applications. Hand-written children are easy to read at five services and tedious at fifty. We switch to generators when the pattern is clearly repeating.

When not to over-engineer. A single cluster with a handful of services does not need a four-level hierarchy. Start with the simplest structure that has clear ownership, and grow it when a real need appears.

A worked example: splitting a growing monorepo

A scale-up starts with one config repository and ten services. At forty services, the production folder has one approval rule for everyone and a single noisy history. Pull requests from unrelated teams conflict constantly. The fix is rarely to explode everything into one repository per service. We usually keep the platform and shared add-ons in one repository owned by the platform team, and give each product area its own application config repository, referenced by an ArgoCD project or a Flux GitRepository.

The split follows ownership, not technology. If two groups never review each other’s changes, they probably should not share a review queue. Moving is mechanical when the layout is clean: copy the folder with history preserved, update the Application source, and confirm the diff is empty before the old path is removed.

Common failure modes

  • Branch per environment. Branches diverge and promotion becomes a merge conflict. We use folders.
  • Everything in one folder. No ownership, no review boundaries, huge diffs.
  • Secrets committed in plain text. Handled with Sealed Secrets or External Secrets Operator, never with exceptions.
  • Copy-pasted overlays. Divergent copies hide real differences. Keep shared config in a base and overlays small.
  • Build pipelines pushing to main. Use a dedicated bot identity, a narrow path, and ideally a pull request for production.

Delivery and handover

You receive the new or restructured repository, a contributing guide that explains where things go and why, CODEOWNERS and branch protection already configured, and CI checks wired to every pull request. We migrate services in slices alongside your engineers, so by the end they have done it themselves at least a few times.

How to tell it is working

Teams rarely need to ask where something lives. Production changes are small, reviewed pull requests. A new environment takes a folder copy rather than a project. If you are unsure which layout fits your team, get in touch and we will review what you have today.

References

How we deliver it

  1. 01

    Inventory what exists

    We list services, teams, clusters, environments and current manifest sources, including Helm releases and loose YAML.

  2. 02

    Define ownership

    We agree who owns platform add-ons, shared configuration and each service, since repository permissions will mirror this.

  3. 03

    Choose the topology

    We pick mono or multi repository, and app-of-apps or generator-driven roots, with the trade-offs written out.

  4. 04

    Lay out environments

    Cluster and environment folders, base and overlay conventions, and naming rules are set before anything is migrated.

  5. 05

    Add guardrails

    CODEOWNERS, required reviews, branch protection and CI validation of manifests are configured on the repository.

  6. 06

    Migrate in slices

    Services move in one by one, starting with a low-risk workload, with the old path kept until the new one proves out.

Tools and technologies

ArgoCDFluxKustomizeHelmGitHubGitLabCODEOWNERSkubeconformkustomize buildpre-commit

Frequently asked questions

What is the app-of-apps pattern?
App-of-apps is an ArgoCD pattern where one parent Application points at a folder containing other Application manifests. Syncing the parent creates all the children. It gives you a single entry point per cluster and makes bootstrapping a new cluster a one-step job. Flux achieves the same with a root Kustomization that points to further Kustomizations.
Should application code and Kubernetes manifests share a repository?
Usually not. A separate config repository keeps deploy changes reviewable on their own, avoids CI loops where a build commits back to its own repo, and lets platform or release owners control production without write access to every code repository. Very small teams sometimes keep both together, and that is acceptable if the pipeline is simple.
Monorepo or one repository per team?
A single config monorepo is easier to search, refactor and enforce standards in. Per-team repositories give stronger isolation and simpler permissions. We lean towards one platform repository for shared add-ons plus either one apps repository or per-team repositories, decided by how much autonomy and separation your teams need.
Should we use branches for environments?
No. Long-lived environment branches drift apart and make promotion a painful merge exercise. Use folders on one branch, with environment differences expressed as overlays or values files. Promotion then becomes a visible change to a file in a pull request, which is easier to review and revert.
Can you restructure our existing repository?
Yes. We map what is deployed today, design the target layout and move services across incrementally so deployments keep working. We never rewrite everything in one commit. Each slice is verified by confirming the controller shows no diff before the old path is removed.

Talk to an engineer about gitops repository structure

Book a free 30-minute call. We review your current setup and tell you honestly whether and how we can help.

Written by Saddique Khan, DevOps / SRE / Cloud Consultant Engineer. Last updated 2026-10-08.