DEV Community

Cover image for Kubernetes: Should You Name Your Controller "foo-bar" or "foobar"? A Survey of 13 Open-Source Projects
suin
suin

Posted on

Kubernetes: Should You Name Your Controller "foo-bar" or "foobar"? A Survey of 13 Open-Source Projects

In this post, I'll share the results of surveying the source code of 13 major open-source projects to answer a question that often comes up when building a Kubernetes Operator: how should you name controllers for multi-word resource types?

When you have a CRD (Custom Resource Definition) type like CertificateRequest or MachineDeployment, should the controller be called foo-bar-controller (hyphenated) or foobar-controller (concatenated lowercase)? There's no official guidance on this. So I dug into real-world, widely used OSS implementations to find out what the de facto standard actually looks like.

What you'll learn from this post

  • How multi-word CRD types are actually named across the ecosystem
  • The conventions 13 OSS projects use for controller names, finalizers, field managers, and more
  • Cross-project trend analysis revealing the most common patterns
  • The convention adopted by a Kubernetes SIG project — the closest thing to an "official" standard

Methodology and scope

For this survey, I cloned the repositories of 13 Kubernetes-related OSS projects and examined their naming conventions at the source code level. These findings are based on actual code, not documentation.

The projects surveyed are listed below.

I looked at six dimensions for each project.

  • Go file and directory names
  • Controller name strings
  • Finalizer names (the mechanism that controls cleanup before a resource is deleted)
  • Field manager names (identifiers for field ownership in Server-Side Apply)
  • Logger context values
  • Reconciler struct names

Master comparison table

Let's start with a side-by-side comparison of all 13 projects.

Project Resource Type Go File / Dir Name Controller Name Finalizer Field Manager Logger Context Reconciler Struct
cert-manager CertificateRequest certificaterequests/ "certificaterequests-issuer-acme" "finalizer.acme.cert-manager.io" "cert-manager-<component>" "certificaterequests-issuer-acme" Controller
Argo CD ApplicationSet applicationset_controller.go "argocd-application-controller" "resources-finalizer.argocd.argoproj.io" Generic (engine-level) "applicationset" ApplicationSetReconciler
Istio VirtualService Custom framework "istio.io/gateway-controller" N/A "pilot-discovery" "KubernetesGateway" autoServiceExportController
Crossplane CompositeResourceDefinition definition/ "defined/compositeresourcedefinition..." "defined.apiextensions.crossplane.io" "apiextensions.crossplane.io/composite" Same as controller name Reconciler
Knative DomainMapping domainmapping/ "domainmapping-controller" "domainmappings.serving.knative.dev" Configurable (no default) "serving.knative.dev.DomainMapping" Reconciler
Strimzi KafkaConnect KafkaConnectAssemblyOperator.java "KafkaConnect" N/A (uses owner refs) N/A (Java) "KafkaConnect(ns/name)" KafkaConnectAssemblyOperator
Prometheus Op PrometheusAgent pkg/prometheus/agent/operator.go "prometheusagent-controller" "monitoring.coreos.com/status-cleanup" "PrometheusOperator" "prometheusagent-controller" Operator
Velero BackupStorageLocation backup_storage_location_controller.go "backup-storage-location" "velero.io/pod-volume-finalizer" "velero-cli" "PodVolumeBackup" (mixed) backupStorageLocationReconciler
Tekton PipelineRun pipelinerun/ "PipelineRun" "chains.tekton.dev/finalizer" Knative defaults "pipelinerun-reconciler" Reconciler
Flux CD HelmRelease helmrelease_controller.go "helm-controller" "finalizers.fluxcd.io" "helm-controller" Auto (controller-runtime) HelmReleaseReconciler
Cluster API MachineDeployment machinedeployment_controller.go "machinedeployment" "cluster.x-k8s.io/machinedeployment" "capi-machinedeployment" "machinedeployment" Reconciler
KubeVirt VirtualMachine vm/vm.go "virt-controller-vm" "kubevirt.io/virtualMachineControllerFinalize" N/A (no SSA) N/A (custom) Controller
Kyverno ClusterCleanupPolicy cleanup/controller.go "cleanup-controller" N/A "kyverno-{suffix}" ControllerLogger(name) controller

Even at a glance, the concatenated lowercase pattern (foobar) stands out. Let's now look at each project in detail.


Project-by-project breakdown

1. cert-manager

cert-manager automates TLS certificate management within Kubernetes clusters. Its multi-word CRD types include CertificateRequest, ClusterIssuer, and CertificateSigningRequest.

Artifact Convention Examples
Go directory name Concatenated lowercase certificaterequests/, certificatesigningrequests/, clusterissuers/
Controller name Concatenated lowercase + hyphenated suffix "certificaterequests-issuer-acme", "certificaterequests-approver", "clusterissuers"
Finalizer Domain-based (not resource-specific) "finalizer.acme.cert-manager.io", "acme.cert-manager.io/finalizer"
Field manager Derived from UserAgent "cert-manager-<component>" (e.g., "cert-manager-test")
Logger context Same as controller name logf.FromContext(ctx, "clusterissuers")
Reconciler struct Controller per package Unexported controller or exported Controller

The key takeaway here is that the multi-word CRD Kind (CertificateRequest) is transformed into concatenated lowercase (certificaterequests). Hyphens only appear when separating functional suffixes like -issuer-acme or -approver. There is one exception: a directory named certificate-shim/ uses a hyphen.

2. Argo CD

Argo CD enables GitOps-based continuous delivery to Kubernetes. ApplicationSet and AppProject are its multi-word CRD types.

Artifact Convention Examples
Go file name Concatenated lowercase + _controller applicationset_controller.go, appcontroller.go
Controller name Hyphenated "argocd-application-controller", "argocd-applicationset-controller"
Finalizer Hyphenated + domain "resources-finalizer.argocd.argoproj.io", "pre-delete-finalizer.argocd.argoproj.io"
Logger context Concatenated lowercase field key log.WithField("applicationset", req.NamespacedName)
Reconciler struct PascalCase ApplicationSetReconciler, ApplicationController

Deployment names use hyphenation (argocd-application-controller), but Go file names and logger keys use concatenated lowercase (applicationset).

3. Istio

Istio provides a service mesh. Its CRD types include VirtualService, DestinationRule, ServiceEntry, and WorkloadEntry.

Artifact Convention Examples
Go file name Concatenated lowercase autoserviceexportcontroller.go, deploymentcontroller.go, configcontroller.go
Controller name Domain + hyphenated "istio.io/gateway-controller", "istio.io/inference-pool-controller", "istio.io/mesh-controller"
Field manager Hyphenated "pilot-discovery", "istio-operator", "istio.io/inference-pool-controller"
Schema plurals Concatenated lowercase "virtualservices", "destinationrules"
Log collection names PascalCase "KubernetesGateway", "HTTPRoute", "GatewayClasses"
Reconciler struct camelCase autoServiceExportController

Istio uses its own custom controller framework. File names are concatenated lowercase, but controller identifier strings use hyphenation. Log collection names preserve PascalCase as-is, which is also notable.

4. Crossplane

Crossplane manages cloud infrastructure through Kubernetes. It has particularly long CRD type names like CompositeResourceDefinition and ManagedResourceActivationPolicy.

Artifact Convention Examples
Go directory name Semantic / abbreviated definition/, activationpolicy/, watchoperation/, cronoperation/
Controller name "prefix/lowercasedKind.group" "defined/compositeresourcedefinition.apiextensions.crossplane.io", "mrap/managedresourceactivationpolicy"
Finalizer Concatenated lowercase as subdomain "defined.apiextensions.crossplane.io", "watchoperation.ops.crossplane.io", "composite.apiextensions.crossplane.io"
Field manager Domain path "apiextensions.crossplane.io/composite", "apiextensions.crossplane.io/managed"
Logger context Controller name value WithValues("controller", controllerName)
Reconciler struct Reconciler per package Reconciler

Controller names use the lowercased Kind directly with no separators at all (compositeresourcedefinition, managedresourceactivationpolicy). Finalizers similarly use concatenated lowercase as subdomains.

5. Knative

Knative is a platform for running serverless workloads on Kubernetes. Its multi-word CRD types include DomainMapping, ServerlessService, PodAutoscaler, KnativeServing, and KnativeEventing.

Artifact Convention Examples
Go directory name Concatenated lowercase domainmapping/, serverlessservice/, knativeserving/, knativeeventing/
Controller name Concatenated lowercase + -controller "domainmapping-controller", "serverlessservice-controller", "podautoscaler-controller", "knativeserving-controller"
Finalizer Concatenated lowercase plural + API group "domainmappings.serving.knative.dev", "serverlessservices.networking.internal.knative.dev", "knativeservings.operator.knative.dev"
Log Kind PascalCase "serving.knative.dev.DomainMapping"
Reconciler struct Reconciler per package (code-generated) reconcilerImpl (generated), Reconciler (user-implemented)

What makes Knative stand out is that its code generator enforces a consistent pattern. Agent names follow the lowercased-kind-controller format uniformly. This is the most programmatic and consistent approach among all 13 projects.

6. Strimzi Kafka Operator

Strimzi is an Operator for running Apache Kafka on Kubernetes. It is implemented in Java and has CRD types such as KafkaConnect, KafkaMirrorMaker2, KafkaBridge, KafkaRebalance, and KafkaNodePool.

Artifact Convention Examples
Java class name PascalCase + AssemblyOperator KafkaConnectAssemblyOperator.java, KafkaMirrorMaker2AssemblyOperator.java
Resource Kind PascalCase "KafkaConnect", "KafkaMirrorMaker2", "KafkaBridge"
Resource plural Concatenated lowercase "kafkaconnects", "kafkamirrormaker2s", "kafkanodepools"
Labels PascalCase values "strimzi.io/kind": "KafkaConnect"
Lock names PascalCase Kind "lock::ns::KafkaConnect::name"
Log markers PascalCase "KafkaConnect(namespace/name)"

Strimzi uses PascalCase (the Kind itself) in most runtime contexts. Plural forms are concatenated lowercase (kafkaconnects), following the standard Kubernetes API convention.

7. Prometheus Operator

Prometheus Operator automates Prometheus operations on Kubernetes. Its CRD types include ServiceMonitor, PodMonitor, ThanosRuler, PrometheusAgent, ScrapeConfig, and AlertmanagerConfig.

Artifact Convention Examples
Go directory / file operator.go per package pkg/alertmanager/operator.go, pkg/prometheus/agent/operator.go, pkg/thanos/operator.go
Controller name Concatenated lowercase + -controller "prometheusagent-controller", "alertmanager-controller", "thanos-controller"
Finalizer Shared (not type-specific) "monitoring.coreos.com/status-cleanup"
Field manager Shared PascalCase "PrometheusOperator"
managed-by label Hyphenated "prometheus-operator"
Logger context Controller name value "component": "prometheusagent-controller"
Reconciler struct Operator per package alertmanager.Operator, thanos.Operator

What's notable here is how multi-word CRD types are concatenated. PrometheusAgent becomes "prometheusagent-controller" — not "prometheus-agent-controller". Additionally, ThanosRuler is shortened to just "thanos-controller".

8. Velero

Velero handles backup and restore for Kubernetes clusters. Its CRD types include BackupStorageLocation, VolumeSnapshotLocation, ServerStatusRequest, PodVolumeBackup, PodVolumeRestore, DataDownload, DataUpload, and DownloadRequest.

Artifact Convention Examples
Go file name Snake case backup_storage_location_controller.go, pod_volume_backup_controller.go, server_status_request_controller.go
Controller name Hyphenated (kebab-case) "backup-storage-location", "pod-volume-backup", "server-status-request", "data-download"
Finalizer Hyphenated + domain "velero.io/pod-volume-finalizer", "velero.io/data-upload-download-finalizer"
Field manager Per-binary "velero-cli"
Logger context Mixed "controller": "PodVolumeBackup" (PascalCase) in constructors vs. "controller": "podvolumebackup" (concatenated) in Reconcile
Reconciler struct Mixed export backupStorageLocationReconciler (unexported), PodVolumeBackupReconciler (exported)

Velero is the only project among all 13 that consistently uses hyphenated word splitting for controller names. Each word from the PascalCase type is separated by a hyphen. Its use of snake case for Go file names is another unique trait not seen in any other project.

9. Tekton Pipelines

Tekton is a framework for building CI/CD pipelines on Kubernetes. Its CRD types include PipelineRun, TaskRun, CustomRun, and ResolutionRequest.

Artifact Convention Examples
Go directory name Concatenated lowercase pipelinerun/, taskrun/, customrun/, resolutionrequest/
Controller name (AgentName) PascalCase (Kind as-is) "PipelineRun", "TaskRun", "CustomRun"
Finalizer Domain-based "chains.tekton.dev/finalizer" (from Tekton Chains)
managed-by Domain path "tekton.dev/pipeline"
Tracer name Concatenated lowercase + -reconciler "pipelinerun-reconciler", "taskrun-reconciler"
Reconciler struct Reconciler per package Reconciler (distinguished by package)

Tekton takes the unique approach of using the PascalCase Kind directly as the controller agent name: "PipelineRun", not "pipelinerun" or "pipeline-run". Directory names and tracer names, on the other hand, use concatenated lowercase.

10. Flux CD

Flux CD is a GitOps toolkit. Its CRD types include GitRepository, HelmRelease, HelmChart, OCIRepository, HelmRepository, and ImageUpdateAutomation.

Artifact Convention Examples
Go file name Concatenated lowercase + _controller.go helmrelease_controller.go, gitrepository_controller.go, ocirepository_controller.go, imageupdateautomation_controller.go
Controller name Per-binary (not per-CRD) "source-controller", "helm-controller", "image-automation-controller"
Finalizer Shared across all types "finalizers.fluxcd.io"
Field manager Binary name "source-controller", "helm-controller"
Reconciler struct PascalCase + Reconciler HelmReleaseReconciler, GitRepositoryReconciler, OCIRepositoryReconciler

Flux doesn't assign per-CRD controller names at all. The controller name is simply the binary name (the executable itself). File names use concatenated lowercase.

11. Cluster API (CAPI)

Cluster API is a Kubernetes SIG project for managing cluster lifecycles. It has a large number of multi-word CRD types: MachineDeployment, MachineSet, MachineHealthCheck, ClusterClass, ClusterResourceSet, ClusterResourceSetBinding, MachinePool, and more.

Artifact Convention Examples
Go file name Concatenated lowercase + _controller.go machinedeployment_controller.go, machinehealthcheck_controller.go, clusterresourceset_controller.go
Go directory name Concatenated lowercase machinedeployment/, machinehealthcheck/, clusterresourceset/
Controller name Concatenated lowercase "machinedeployment", "machinehealthcheck", "clusterresourceset", "clusterresourcesetbinding"
Event recorder Concatenated lowercase + -controller "machinedeployment-controller", "machinehealthcheck-controller"
Finalizer Concatenated lowercase in domain "cluster.x-k8s.io/machinedeployment", "machinedeployment.topology.cluster.x-k8s.io", "machinepool.cluster.x-k8s.io"
Field manager (SSA) capi- + concatenated lowercase "capi-machinedeployment", "capi-machineset"
Logger context Concatenated lowercase WithValues("controller", "machinedeployment"), WithValues("controller", "clusterresourcesetbinding")
SSA cache Concatenated lowercase ssa.NewCache("machinedeployment")
Reconciler struct Reconciler per package machinedeployment.Reconciler, machinehealthcheck.Reconciler

Cluster API has the most comprehensive and consistent naming convention of all 13 projects. Every single artifact uses concatenated lowercase for the resource type. MachineDeployment becomes machinedeployment everywhere, without exception.

As a Kubernetes SIG project, this convention can be considered the closest thing to an "official" standard.

12. KubeVirt

KubeVirt enables running virtual machines on Kubernetes. It has particularly long CRD type names: VirtualMachine, VirtualMachineInstance, VirtualMachineInstanceReplicaSet, VirtualMachinePool, and VirtualMachineInstanceMigration.

Artifact Convention Examples
Go directory / file Abbreviated vm/vm.go, vmi/vmi.go, replicaset/replicaset.go, pool/pool.go, migration/migration.go
Work queue name Abbreviated + hyphenated prefix "virt-controller-vm", "virt-controller-vmi", "virt-controller-replicaset"
Event recorder Concatenated lowercase + -controller "virtualmachine-controller", "virtualmachinereplicaset-controller", "virtualmachinepool-controller"
Finalizer camelCase "kubevirt.io/foregroundDeleteVirtualMachine", "kubevirt.io/virtualMachineControllerFinalize", "kubevirt.io/migrationJobFinalize"
Controller struct Controller per package vm.Controller, vmi.Controller, migration.Controller

KubeVirt uses abbreviations (VM, VMI) aggressively in its internal code, while event recorders use concatenated lowercase (virtualmachine). Its use of camelCase strings for finalizers is unique among all 13 projects.

13. Kyverno

Kyverno is a policy engine for Kubernetes. Its CRD types include ClusterPolicy, ClusterCleanupPolicy, PolicyReport, and GlobalContextEntry.

Artifact Convention Examples
Go directory name By functional concern cleanup/, policycache/, policystatus/, globalcontext/
Controller name Hyphenated (concern-based) "cleanup-controller", "policycache-controller", "global-context", "admissionpolicy-generator"
Field manager Prefixed "kyverno-{suffix}"
Logger context Controller name logging.ControllerLogger(ControllerName)
Reconciler struct Unexported generic name controller (per package)

What distinguishes Kyverno is that controllers are named by functional concern rather than CRD type. When multi-word concepts appear, it mixes concatenated lowercase (policycache, admissionpolicy) with hyphenation (global-context), resulting in some inconsistency.


Cross-project analysis

Based on the individual findings above, let's now analyze overall trends by artifact type.

Go file and directory names

Pattern Projects Using It Share
Concatenated lowercase (foobar_controller.go, foobar/) cert-manager, Argo CD, Knative, Tekton, Flux, Cluster API, Crossplane, Prometheus Op ~70%
Snake case (foo_bar_controller.go) Velero ~8%
Abbreviated (vm.go) KubeVirt ~8%
By concern (controller.go in a semantic directory) Kyverno, Crossplane (partially) ~15%

Concatenated lowercase is the overwhelming favorite. FooBar becomes foobar_controller.go or foobar/controller.go.

Controller name strings

Pattern Projects Using It Examples
Concatenated lowercase (bare or + -controller) Cluster API, Knative, Prometheus Op, cert-manager, Crossplane "machinedeployment", "domainmapping-controller", "prometheusagent-controller"
Hyphenated (word-split) Velero "backup-storage-location", "pod-volume-backup"
PascalCase (Kind as-is) Tekton, Strimzi, Flux (logger only) "PipelineRun", "KafkaConnect"
Binary name (not per-CRD) Flux, Istio "helm-controller", "pilot-discovery"
By concern Kyverno "cleanup-controller"

Again, concatenated lowercase is the most common. FooBar becomes "foobar" or "foobar-controller".

Finalizer names

Pattern Projects Using It Examples
Concatenated lowercase in domain Cluster API, Knative, Crossplane "cluster.x-k8s.io/machinedeployment", "domainmappings.serving.knative.dev"
Shared / generic Flux, Prometheus Op, cert-manager "finalizers.fluxcd.io", "monitoring.coreos.com/status-cleanup"
Hyphenated Velero "velero.io/pod-volume-finalizer"
camelCase KubeVirt "kubevirt.io/virtualMachineControllerFinalize"

Field manager names

Pattern Projects Using It Examples
Prefix + concatenated lowercase Cluster API "capi-machinedeployment"
Binary name Flux, Istio "source-controller", "pilot-discovery"
Shared operator name Prometheus Op "PrometheusOperator"
Domain path Crossplane "apiextensions.crossplane.io/composite"

Reconciler struct names

Pattern Projects Using It Share
Generic Reconciler per package Cluster API, Knative, Tekton, Crossplane ~40%
PascalCase FooBarReconciler Flux, Argo CD ~20%
Operator per package Prometheus Op ~8%
Controller per package KubeVirt, cert-manager ~15%
Unexported controller Kyverno ~8%

Conclusions

The dominant convention: concatenated lowercase

The clear winner across all naming dimensions is concatenated lowercase — lowercasing the PascalCase Kind with no separator.

FooBarfoobar

Here's a breakdown of adoption rates for this pattern.

  • ~70% of projects use it for file/directory names
  • ~50% of projects use it for controller name strings
  • ~40% of projects use it for finalizer domain components
  • 100% of projects use it for Kubernetes API resource plurals (this is a Kubernetes API standard itself)

Who uses hyphenation?

Only Velero consistently word-splits CRD types with hyphens ("backup-storage-location", "pod-volume-backup") — that's 1 out of 13 projects.

Kyverno partially uses hyphens ("global-context"), but also mixes in concatenated lowercase (policycache, admissionpolicy), so it is not consistent.

The -controller suffix

When appending a -controller suffix to a name, every single project connects it with a hyphen. It's always "foobar-controller", never "foobarcontroller". This is universal.

Recommended naming convention

Based on the survey results, here is a recommended set of conventions. The Cluster API conventions (a Kubernetes SIG project) are particularly worth following, as they are the most comprehensive and consistent of any project surveyed.

Artifact Recommended Convention Example for FooBar
Go file name Concatenated lowercase + underscore + controller.go foobar_controller.go
Go directory Concatenated lowercase foobar/
Controller name Concatenated lowercase "foobar"
Controller name (with suffix) Concatenated lowercase + hyphen + controller "foobar-controller"
Finalizer Concatenated lowercase in domain "example.io/foobar" or "foobar.example.io"
Field manager Prefix + concatenated lowercase "myoperator-foobar"
Logger context Concatenated lowercase "foobar"
Reconciler struct PascalCase or generic per package FooBarReconciler or Reconciler

Why concatenated lowercase wins

There are several reasons why concatenated lowercase is so widely adopted.

First, it aligns with the Kubernetes API's own conventions. The standard resource plurals are already concatenated lowercase: deployments, replicasets, statefulsets, daemonsets. CRD resources naturally follow this same pattern.

Second, it fits well with Go package naming conventions. Go packages are conventionally single lowercase words, and a name like machinedeployment fits right in.

Third, it eliminates ambiguity. With hyphenation, splitting FooBar into foo-bar is straightforward, but FooBarBaz could plausibly be foo-bar-baz or foo-barbaz — the word boundaries aren't always obvious. With concatenated lowercase, you simply lowercase the Kind as-is. It's a mechanical transformation with no room for interpretation.

And perhaps the most compelling reason: Cluster API, a Kubernetes SIG project, uses this convention with complete consistency. If you want to follow the crowd in the Kubernetes ecosystem, this is the convention to pick.

Hyphenation has its merits too

That said, hyphenation has legitimate advantages.

There's no denying that backup-storage-location is easier for humans to read than backupstoragelocation. In contexts where humans read names directly — deployment names, CLI output, log messages — readability is a significant benefit.

Additionally, hyphenated names are the standard convention in Kubernetes label values, annotation keys, and resource names in manifests.

In other words, it comes down to a choice: concatenated lowercase for consistency with APIs and code, or hyphenation for human readability. Neither is wrong — but the ecosystem has spoken, and concatenated lowercase is the overwhelming favorite.


Final thoughts

In this post, I surveyed the source code of 13 Kubernetes-related OSS projects and mapped out the real-world naming conventions for multi-word CRD types. The verdict: concatenated lowercase (foobar) is the de facto standard, with Velero being the only project that consistently uses hyphenation (foo-bar).

If you're building a Kubernetes Operator and unsure which convention to follow, Cluster API's approach is a safe bet. That said, there's no absolute right answer here. The most pragmatic approach is to prioritize consistency within your own project and go with whatever your team finds readable.

Thanks for reading to the end! I tweet about tech topics that don't make it into my blog posts, so feel free to follow me if you're interested → Twitter@suin

Top comments (0)