App-of-Apps vs ApplicationSet in Argo CD: Which Pattern to Use and When

Quick answer
Both App-of-Apps and ApplicationSet let one Argo CD Application manage many others — but they solve different problems. Here's the real decision: App-of-Apps for an explicit, hand-curated tree; ApplicationSet for generated fleets you don't want to maintain by hand.
10 min read · Kubernetes
If you run Argo CD past a handful of Applications, you eventually hit the same question: how do you manage many Applications without hand-writing and hand-syncing each one? Argo CD gives you two answers — the App-of-Apps pattern and the ApplicationSet controller — and they are constantly compared as if they were alternatives to the same problem. They're not.
App-of-Apps is a convention: a single Application whose Git source contains more Application manifests. ApplicationSet is a controller that generates Application resources from a template plus a generator. The short version of the decision: use App-of-Apps when you want an explicit, hand-curated list of Applications under one root, and ApplicationSet when you want Applications produced automatically from clusters, Git directories, or a list — so you never touch them by hand.
This post walks through how each actually works, where App-of-Apps quietly becomes a liability, and the decision rule I use on real platforms.
How App-of-Apps Works
App-of-Apps is just Argo CD pointed at itself. You create one "root" Application whose source is a Git path containing more Application YAML. When the root syncs, those child Applications are created in the cluster, and each child then syncs its own workload.
1apiVersion: argoproj.io/v1alpha1
2kind: Application
3metadata:
4 name: root
5 namespace: argocd
6spec:
7 project: platform
8 source:
9 repoURL: https://github.com/acme/gitops
10 path: apps # this directory contains more Application manifests
11 targetRevision: main
12 destination:
13 server: https://kubernetes.default.svc
14 namespace: argocd
15 syncPolicy:
16 automated:
17 prune: true
18 selfHeal: trueInside apps/ you commit one file per child:
1# apps/cert-manager.yaml
2apiVersion: argoproj.io/v1alpha1
3kind: Application
4metadata:
5 name: cert-manager
6 namespace: argocd
7spec:
8 project: platform
9 source:
10 repoURL: https://github.com/acme/gitops
11 path: addons/cert-manager
12 targetRevision: main
13 destination:
14 server: https://kubernetes.default.svc
15 namespace: cert-manager
16 syncPolicy:
17 automated: { prune: true, selfHeal: true }The whole thing is recursive and explicit. Every Application that exists is a file in Git that a human wrote. That is App-of-Apps' biggest strength and, at scale, its biggest weakness.
What it's good at: a curated platform bundle — the set of addons every cluster gets (cert-manager, external-secrets, ingress, monitoring). The list is small, intentional, and changes deliberately. Sync waves (argocd.argoproj.io/sync-wave) let you order installs, so CRDs land before the controllers that need them. You can read the entire topology by reading one directory.
Where it breaks down: the moment "the list" stops being hand-maintainable. Onboard a tenth cluster and you copy-paste ten files and rewrite the destination.server in each. Add a new team namespace and you author another Application by hand. The pattern has no concept of "for each X, make an Application" — so every axis of growth (clusters, teams, regions, environments) multiplies the YAML you maintain manually. You also get a slightly awkward two-step delete: pruning a child requires the root to prune, and finalizers on the child to cascade the workload deletion.
How ApplicationSet Works
ApplicationSet inverts the model. Instead of you writing each Application, you write one template and a generator that produces the parameters. The controller renders one Application per generated parameter set and keeps them reconciled — create, update, and delete — as the generator's inputs change.
1apiVersion: argoproj.io/v1alpha1
2kind: ApplicationSet
3metadata:
4 name: addons
5 namespace: argocd
6spec:
7 goTemplate: true
8 generators:
9 - clusters: {} # one element per cluster registered in Argo CD
10 template:
11 metadata:
12 name: 'cert-manager-{{.name}}'
13 spec:
14 project: platform
15 source:
16 repoURL: https://github.com/acme/gitops
17 path: addons/cert-manager
18 targetRevision: main
19 destination:
20 server: '{{.server}}'
21 namespace: cert-manager
22 syncPolicy:
23 automated: { prune: true, selfHeal: true }Register a new cluster in Argo CD and cert-manager appears on it automatically — no new file, no PR to an apps/ directory. That is the entire point: the fleet maintains itself from a source of truth that isn't a hand-written list.
The power is in the generators, which is where ApplicationSet earns its complexity:
- List — a literal array of parameters. The simplest generator; basically App-of-Apps without the per-file boilerplate.
- Cluster — one Application per cluster registered in Argo CD, with label selectors to target subsets (
prodvsstaging). - Git (directories / files) — one Application per subdirectory or per config file in a repo. This is how you do "every folder under
tenants/becomes an Application" — add a folder, get an Application. - Matrix / Merge — compose generators. Matrix takes the cartesian product (e.g. every addon × every cluster) to render a grid; Merge combines parameter sets by a merge key, letting a later generator override values from an earlier one.
- Pull Request / SCM Provider — generate Applications per open PR or per repo in an org, for preview environments and org-wide rollouts.
For careful, ordered rollouts across the fleet, ApplicationSet has progressive syncs (spec.strategy.type: RollingSync), so you can update canary clusters before production ones — something App-of-Apps cannot express at all. One caveat: progressive syncs have long been an opt-in feature, enabled with the --enable-progressive-syncs controller flag (or the equivalent ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_PROGRESSIVE_SYNCS env var), so confirm it's turned on for your Argo CD version before relying on it. I covered that in depth in Argo CD ApplicationSet Progressive Syncs, and the multi-cluster mechanics in Argo CD ApplicationSet for Multi-Cluster.
Where it bites: a templating bug or a bad generator can render — or delete — Applications across your whole fleet at once. Blast radius is the price of automation. You also debug through a layer of indirection: when an Application looks wrong, you're often reading generator output and Go templates, not a static file you can diff in a PR.
Kubernetes Production Readiness Checklist
The pre-launch checks we run before calling a cluster production-ready — probes, resources, RBAC, upgrades, and backups. Plain Markdown you can commit to your repo.
Free. Instant download. You'll also get the occasional deep-dive from the newsletter — unsubscribe anytime.
App-of-Apps vs ApplicationSet: The Real Differences
| Dimension | App-of-Apps | ApplicationSet |
|---|---|---|
| Mechanism | Convention — an Application sourcing more Applications | Controller — generates Applications from a template |
| Source of truth | A hand-written directory of Application files | A generator (clusters, Git dirs, list, PRs, matrix) |
| Adding an Application | Author and commit a new file | Happens automatically when generator input changes |
| Scales along | Manually, one file per item | Automatically, per cluster / dir / PR |
| Ordered rollout | Sync waves within a cluster | Progressive syncs across clusters |
| Blast radius | Low — changes are explicit and per-file | High — one template change touches every render |
| Debuggability | Read the directory; it's all there | Inspect generator output + template rendering |
| Best for | A curated, slow-changing platform bundle | Fleets that grow along an axis you don't want to hand-maintain |
The cleanest way to hold the distinction: App-of-Apps is a list you maintain; ApplicationSet is a list you generate. If a human should decide each entry, App-of-Apps keeps that decision explicit and reviewable. If the entries are mechanically derived from something else — your cluster inventory, a tenants folder, open PRs — then maintaining that list by hand is just toil ApplicationSet exists to delete.
Which Should You Use?
Here is the decision rule I actually apply:
- One cluster, a fixed set of platform addons? Use App-of-Apps. It's explicit, trivially reviewable, and ApplicationSet would be ceremony with no payoff. Sync waves handle ordering.
- The same set of apps across many clusters? Use an ApplicationSet with the cluster generator. This is the canonical case ApplicationSet was built for, and where App-of-Apps' copy-paste tax hurts most.
- Per-tenant or per-team Applications driven by a folder structure? Use an ApplicationSet with the Git directory generator — add a folder, get an Application.
- Preview environments per pull request? Use an ApplicationSet with the Pull Request generator. App-of-Apps has no equivalent.
- A handful of curated apps that nonetheless need ApplicationSet's lifecycle (e.g. you want one place to template repeated fields)? Use the list generator — it's ApplicationSet with the explicitness of App-of-Apps.
And the combination that most mature platforms land on: both. A root App-of-Apps bootstraps the platform and includes the ApplicationSets themselves as children. The root tree stays small and human-curated (the platform's "operating system"), while the ApplicationSets fan out the parts that scale by cluster, tenant, or PR. You're not choosing a side — you're using App-of-Apps for the curated core and ApplicationSet for the generated fleet.
If you're still standing up the basics first, GitOps with Argo CD: Production Setup and the hands-on App-of-Apps pattern tutorial are the right starting points before you reach for generators.
Frequently Asked Questions
Is ApplicationSet a replacement for App-of-Apps?
No. ApplicationSet generates Applications from a template; App-of-Apps is a hand-curated tree of Applications. They overlap only for the trivial "static list" case (where ApplicationSet's list generator does the same job with less boilerplate). For curated platform bundles, App-of-Apps is still perfectly valid — and the two are routinely combined.
Is ApplicationSet still a separate install in current Argo CD?
No. The ApplicationSet controller was merged into Argo CD core and ships with it (it has since the 2.3+ era). You don't install a separate component for current versions — the ApplicationSet CRD and controller come with a standard Argo CD install.
Can I use App-of-Apps and ApplicationSet together?
Yes, and it's the common production pattern. A root App-of-Apps bootstraps the platform and includes your ApplicationSet manifests as children. The root stays small and explicit; the ApplicationSets handle everything that scales per cluster, tenant, or PR.
How do I order resource creation with each pattern?
App-of-Apps uses sync waves (argocd.argoproj.io/sync-wave) to order children and resources within a sync — useful for CRDs-before-controllers. ApplicationSet adds progressive syncs (RollingSync) to order rollouts across clusters — useful for canarying a change to staging clusters before production ones.
Why did my ApplicationSet delete a bunch of Applications?
Because the generator's input changed and the controller reconciled to match. A cluster getting unregistered, a Git directory being removed, or a label selector being edited will prune the Applications that no longer match. This is the blast-radius trade-off. The guardrail for this case is the applicationsSync policy — create-only or create-update stops the controller from deleting generated Applications when results change (the controller-level --policy flag enforces the same globally). Note that preserveResourcesOnDeletion is a different lever: it only protects resources when the ApplicationSet object itself is deleted, not when a generator's input changes. Either way, review ApplicationSet changes as carefully as you'd review an rm -rf.
Does App-of-Apps work across multiple clusters?
It can, but awkwardly — each child Application sets its own destination.server, so multi-cluster means hand-writing the cluster targeting in every file. That manual per-cluster duplication is exactly the toil the ApplicationSet cluster generator removes, which is why multi-cluster is the textbook reason to switch.
For the broader GitOps tooling decision, see Argo CD vs Flux CD: A Detailed Comparison. For scaling generated Applications across a fleet, see Argo CD ApplicationSet for Multi-Cluster and Argo CD ApplicationSet Progressive Syncs.
Designing GitOps for a multi-cluster, multi-team platform? Talk to us at Coding Protocols — we help platform teams build Argo CD topologies that stay reviewable as they scale.
Official References
- Argo CD declarative setup — the Application and AppProject spec
- Argo CD sync options — prune, self-heal and sync-wave behaviour
Was this article helpful?
Be the first to rate this article
Related Topics
Found this useful? Share it.


