Kubernetes Control Loop Pattern
The single most leverage-laden concept in Kubernetes. Every behavior in the platform — Deployments managing ReplicaSets, the scheduler binding Pods to Nodes, the kubelet starting containers, the HorizontalPodAutoscaler adjusting replicas, the cert-manager Operator rotating TLS certificates, even garbage collection — is an instance of the same pattern: a controller watches the API server’s representation of cluster state, diffs the observed state against the declared desired state, and takes an action that moves observed closer to desired, repeating indefinitely. The Kubernetes documentation defines this pattern with deliberate plainness: “In Kubernetes, controllers are control loops that watch the state of your cluster, then make or request changes where needed. Each controller tries to move the current cluster state closer to the desired state” (kubernetes.io — Controllers). What makes the K8s implementation distinctive — and what separates “a control loop” from “a control loop that survives the realities of distributed computing” — is that it is level-triggered rather than edge-triggered: controllers react to the current state of the world on every wake-up rather than to the events announcing that the state changed. This single design choice, articulated by James Bowes in the canonical 2017 essay Level Triggering and Reconciliation in Kubernetes (hackernoon.com), is what makes Kubernetes robust to dropped events, network partitions, controller restarts, replayed messages, and the entire menagerie of distributed-system failures that would defeat an edge-triggered scheduler. Joe Beda, one of Kubernetes’ three founders, summarizes it: “The goal-seeking behavior of the control loop is very stable. … If you are level triggered the pattern is very forgiving” (Bowes 2017). This note is the conceptual core of the K8s vault; every other note that mentions a controller, a reconciler, an operator, an HPA, or a scheduler implicitly inherits its model from here.
Mental Model
flowchart LR subgraph "Single Controller's Reconcile Loop" WATCH[Watch<br/>list-watch API server<br/>via informer cache] QUEUE[Workqueue<br/>deduplicate + rate-limit] RECONCILE[Reconcile<br/>read current state<br/>compute desired delta<br/>act idempotently] STATUS[Update Status<br/>via /status subresource] REQUEUE[Requeue<br/>error → exp backoff<br/>RequeueAfter → schedule] end APISERVER[(kube-apiserver<br/>+ etcd)] EXTERNAL[(External world<br/>cloud APIs, nodes,<br/>container runtimes, DNS)] APISERVER -- "watch events:<br/>ADD / MOD / DEL" --> WATCH WATCH --> QUEUE QUEUE -- "object key<br/>(namespace/name)" --> RECONCILE RECONCILE -- "GET current state" --> APISERVER RECONCILE -- "CREATE / UPDATE / PATCH children" --> APISERVER RECONCILE -- "side effects:<br/>provision LB, attach disk,<br/>pull image, etc." --> EXTERNAL RECONCILE --> STATUS STATUS --> APISERVER RECONCILE --> REQUEUE REQUEUE --> QUEUE
What this diagram shows. A single Kubernetes controller is a small state machine wrapped around four primitives: a watch that subscribes to API server events for one or more resource types, a workqueue that deduplicates and rate-limits incoming events, a reconcile function that reads current state and acts to converge it toward desired state, and a requeue mechanism that schedules the next iteration. The reconcile function is the only place where business logic lives; everything else is plumbing. Crucially, the reconcile function receives only an object identity (namespace/name) — not a diff, not the event payload, not “what changed.” Each invocation independently reads the current state from the informer cache (a local read of the API server’s state populated by the watch stream) and computes what action is needed to bring it toward desired. This is the level-triggered model in operation: the reconciler treats every invocation as a fresh comparison of declared vs observed, never relying on knowing what specific event woke it up. The insight to extract is that the controller is correct because it is forgetful: it carries no state between invocations beyond what is durably stored in the API server, so missed events, replayed events, controller restarts, and out-of-order events all converge to the same fixed point.
Mechanical Walk-through
The watch / list-watch protocol
A controller does not poll the API server. It opens a watch — a long-lived HTTP/2 streaming connection to the API server — that delivers events as JSON objects whenever resources of interest change. The watch is parameterized by a resourceVersion token (see Resource Versioning and Optimistic Concurrency); the server returns all subsequent changes from that point. If the connection drops or the server expires the token (the etcd compaction window), the client falls back to a list — a full enumeration of current state — and re-opens the watch from the returned resourceVersion. This list-watch loop is the canonical I/O pattern of every K8s controller and is implemented once in the client-go informer library, then reused by every Go-based controller in the ecosystem.
The events delivered are of three types: ADDED, MODIFIED, DELETED. They are hints — they tell the controller “something happened to this object” — but the controller does not take action based on the event payload. Instead, the controller-runtime / client-go informer maintains a local in-memory cache of all watched objects (the “informer cache” or “shared informer”). On every event, the cache is updated and the object’s {namespace, name} key is pushed into a workqueue. The reconciler later dequeues the key, reads the current (now-cached) state, and acts. The event payload is discarded; only the key matters. This is what makes the pattern level-triggered rather than edge-triggered.
The workqueue
Between the watch and the reconcile, there is a rate-limited deduplicating workqueue. The workqueue ensures:
- Deduplication. If a Pod is modified five times in two seconds, the queue holds one entry for that Pod and the reconciler runs once with the latest state. The Kubebuilder Book example phrases this directly: “a user creates a ReplicaSet with 1000 replicas … the Controller batches the Pod updates together (the Reconcile only gets the ReplicaSet Namespace and Name) before triggering the Reconcile” (Kubebuilder Book).
- Rate limiting. Failed reconciliations are re-queued with exponential backoff (typically starting at 5 ms and capping at hours). This prevents a single broken resource from saturating the controller.
- Priority and ordering. Newer controller-runtime versions support priority queues; the default is FIFO.
The deduplication property is essential for correctness and performance. Without it, a controller would have to process every event in order, multiplying CPU and API server load with cluster activity. With it, a controller scales primarily with the number of distinct objects under management, not with the event rate.
The reconcile function
The reconcile function is the controller’s business logic. Its signature in sigs.k8s.io/controller-runtime is (pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/reconcile):
type Reconciler interface {
Reconcile(context.Context, Request) (Result, error)
}
type Request struct {
types.NamespacedName // Name and Namespace ONLY — no event payload
}
type Result struct {
Requeue bool // requeue with default rate limiter (deprecated; use RequeueAfter)
RequeueAfter time.Duration // requeue after this delay (e.g., 30 * time.Second)
Priority *int // priority if using PriorityQueue
}The reconcile function returns one of four outcomes:
| Return value | Meaning |
|---|---|
(Result{}, nil) | Reconciliation complete; do not requeue (until next watch event fires). |
(Result{RequeueAfter: d}, nil) | Schedule a re-reconcile in d (e.g., for periodic checks against external systems). |
(Result{}, err) | Requeue with exponential backoff for retry. The default backoff starts at 5 ms and caps at ~16 minutes. |
(Result{}, TerminalError(err)) | Treat as terminal; do not requeue. Available since controller-runtime v0.15.0. |
The documentation states the design principle plainly: “Reconciliation is level-based, meaning action isn’t driven off changes in individual Events, but instead is driven by actual cluster state read from the apiserver or a local cache. For example if responding to a Pod Delete Event, the Request won’t contain that a Pod was deleted, instead the reconcile function observes this when reading the cluster state and seeing the Pod as missing” (controller-runtime — Reconcile). This is the idempotency-by-construction property of level-triggered controllers: the same reconcile logic that handles a fresh creation handles a retry, a controller restart, and a missed event identically — because all of them just read current state and act.
Status reporting
After reconciling, the controller typically updates the resource’s status subresource to reflect what it observed and what it did. The /status subresource is a separate REST endpoint (PATCH /apis/<group>/<version>/namespaces/<ns>/<resource>/<name>/status) that exists specifically to let controllers update status without triggering a metadata.generation increment (which is reserved for spec changes). The Kubernetes documentation gives the canonical example: “Controllers also update the objects that configure them. For example: once the work is done for a Job, the Job controller updates that Job object to mark it Finished” (kubernetes.io — Controllers). The status update is the controller’s way of telling the rest of the system — other controllers, the user, monitoring — what the world looks like from this controller’s perspective. The conceptual partner to this update is Desired State vs Observed State.
Periodic resync
Even with no events, controllers re-reconcile every object in their cache on a periodic interval (default 10 hours in controller-runtime, configurable). This “resync” is the safety net for any event that might have been missed despite the level-triggered design — long network partitions, informer-cache desync, version-skew bugs in the watch handler. The resync is invisible to the reconcile function (it just sees another reconcile call with the same identity) but it ensures the system eventually converges even under adversarial conditions. Combined with periodic re-reconciliation triggered by RequeueAfter, the resync gives every controller a “heartbeat” against actual state that no event-driven system can match.
Level-Triggered vs Edge-Triggered: The Foundational Distinction
The level-triggered design is the single most important property of the Kubernetes controller model. To see why, contrast it with an edge-triggered alternative.
Edge-triggered. The controller reacts to each event as it arrives. Event: “Pod my-pod was deleted.” Action: “decrement my-replicaset’s replica count and create a new Pod.” This is the natural pattern for queue-driven systems (Kafka consumers, RabbitMQ workers, AWS SQS handlers) and the natural intuition for “event-driven” software. Edge-triggered systems are efficient — they do exactly the work necessitated by each event.
Level-triggered. The controller reacts to the current state of the world. The reconciler computes, on each wake-up: “How many Pods do I currently have? How many do I want? Create or delete the difference.” This is the natural pattern for thermostats, cruise control, robotics, and electronic flip-flops — the term itself comes from digital electronics, where “level-triggered” means the latch responds to whether the signal is currently high vs. when the signal transitioned.
The Bowes essay (medium.com/hackernoon — Level Triggering and Reconciliation in Kubernetes) gives a sharp arithmetic example. Imagine a = 3 initially, and the user wants a = 7:
- Edge-triggered:
add 4 to a. The operation happens once, at the moment of the command. - Level-triggered:
a is 7. The desired value persists; the system continuously compares actual to 7 and adjusts.
Now consider a scaling scenario: the user wants 1 replica, then 5, then 2.
- Edge-triggered: the system receives
+4then-3. If a network partition delays the+4so it arrives concurrently with-3, and the system has only realized 3 of the 4 adds when-3fires, the controller might subtract 3 from a state of 4 and arrive at 1 — instead of the desired 2. Worse: if the+4is lost entirely, the system stays at 1; the user’s intent is silently violated. - Level-triggered: the desired state is
replicas: 5and thenreplicas: 2. On every wake-up the controller reads the current count and the desired count and acts to make them match. Lost events don’t matter — the next wake-up reads the current desired state and converges. Partial executions don’t matter — the next wake-up reads the current actual state and resumes.
The Bowes essay summarizes the deeper point: “Level triggered systems handle network partitions, missed events, and clumsy human operators much more gracefully. Disruptions that cause edge-triggered systems to diverge from intended outcomes don’t compromise level-triggered systems as severely.” This is why Kubernetes’ control loops can survive (a) the API server restarting, (b) etcd briefly becoming unreachable, (c) a controller pod being killed and restarted, (d) watch streams being dropped and re-listed from a snapshot, (e) clock skew, (f) network partitions of arbitrary duration. None of these scenarios require any special handling in the reconcile function — the function just reads current state next time it runs.
The trade-off the level-triggered design pays: inefficiency under high event rates. An edge-triggered system that receives 1,000 Pod-update events does 1,000 incremental updates. A level-triggered system reads the current state 1,000 times (or once, after deduplication via the workqueue) and recomputes the full desired-state action each time. For most K8s workloads the trade is favorable because (a) the workqueue collapses bursts, (b) reads from the informer cache are fast, (c) the reconcile function is typically dominated by external API calls (cloud, container runtime) that would be unsafe to issue blindly per event anyway. For very high-event-rate scenarios (millions of objects, sub-millisecond change rates), the model strains — see the discussion of kube-state-metrics performance at scale.
The dual-mode reality of well-written controllers is that they are driven by watch events (so they react quickly to changes) but act on cache reads (so they are correct under missed events). This is the optimization the Bowes essay calls out as the practical compromise: efficient triggering via edges, correct action via levels.
Configuration: A Minimal Controller in controller-runtime
The canonical Go-based controller is constructed via the kubebuilder / controller-runtime scaffold (book.kubebuilder.io). Here is the minimal reconcile function for a hypothetical MySQLBackup CRD, annotated:
package controllers
import (
"context"
"time"
backupv1 "myorg.io/api/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
)
// MySQLBackupReconciler reconciles a MySQLBackup object.
type MySQLBackupReconciler struct {
client.Client // K8s API client (read/write)
Scheme *runtime.Scheme // owns the CRD's Go-type registry
}
func (r *MySQLBackupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
// 1. READ: fetch the current MySQLBackup object. The request carries only
// {Namespace, Name} — no event details. We read the live state from the
// informer cache (a local read against the watch-populated cache).
var backup backupv1.MySQLBackup
if err := r.Get(ctx, req.NamespacedName, &backup); err != nil {
if apierrors.IsNotFound(err) {
// The object has been deleted. The owner-references graph and
// finalizers handle cleanup of children; we just return.
return ctrl.Result{}, nil
}
return ctrl.Result{}, err // transient API error → exponential backoff
}
// 2. HANDLE DELETION: if the object has a deletion timestamp, run finalizer
// logic (delete external resources, then remove our finalizer).
if !backup.DeletionTimestamp.IsZero() {
return r.finalizeBackup(ctx, &backup)
}
// 3. DIFF + ACT: compare desired state to observed state and converge.
// For a MySQLBackup, "observed state" is "does the backup Job exist
// and what is its status?" We GET the Job and create/update as needed.
var job batchv1.Job
err := r.Get(ctx, types.NamespacedName{
Namespace: backup.Namespace,
Name: backup.Name + "-job",
}, &job)
if apierrors.IsNotFound(err) {
// Desired Job doesn't exist → create it.
newJob := r.buildBackupJob(&backup)
if err := ctrl.SetControllerReference(&backup, newJob, r.Scheme); err != nil {
return ctrl.Result{}, err
}
if err := r.Create(ctx, newJob); err != nil {
return ctrl.Result{}, err // retry on backoff
}
logger.Info("created backup Job", "job", newJob.Name)
} else if err != nil {
return ctrl.Result{}, err
}
// Note: if the Job exists and matches our spec, we do nothing — idempotent.
// 4. STATUS UPDATE: reflect what we observed in the .status subresource.
backup.Status.Phase = inferPhaseFromJob(&job)
backup.Status.LastObservedAt = metav1.Now()
if err := r.Status().Update(ctx, &backup); err != nil {
return ctrl.Result{}, err
}
// 5. REQUEUE: schedule next reconcile. For backups, poll the Job's progress
// every 30s until completion.
if !isJobComplete(&job) {
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}
return ctrl.Result{}, nil // terminal success
}
// SetupWithManager registers the controller with the manager and tells it
// what resources to watch.
func (r *MySQLBackupReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&backupv1.MySQLBackup{}). // primary resource (watch)
Owns(&batchv1.Job{}). // owned children (watch + auto-trigger)
Complete(r)
}Line-by-line:
- (1) Read. The reconcile function knows only the object’s identity. The first call is a
Getagainst the API server (served from the informer cache for free). ANotFoundmeans the object was deleted between the event firing and the reconcile dequeuing the key — perfectly normal under level-triggered design. - (2) Handle deletion. Finalizers (covered in Finalizers) gate deletion until external cleanup is done. The reconciler is responsible for checking
DeletionTimestampand running cleanup before removing the finalizer. - (3) Diff and act. The reconciler reads the current state of the world (does the Job exist?), computes the desired state (the Job should exist), and acts to converge. If both already agree, nothing happens — the reconcile is a no-op. This is the idempotency property.
- (4) Status update. The
Status().Update()call writes to the/statussubresource, which does not incrementmetadata.generation(preserving the convention thatgenerationtracks user-modified spec changes). This is what tells the rest of the system what the controller has observed. - (5) Requeue. Returning
RequeueAfterschedules the next call without needing a watch event to fire it. Errors trigger exponential backoff automatically. The combination handles every requeue case the controller needs.
Every controller in the Kubernetes ecosystem — from the built-in Deployment controller in kube-controller-manager, to cert-manager, to the Prometheus Operator, to ArgoCD’s Application controller — is structured this way. The pattern is so universal that controller-runtime essentially generates 90 % of a working controller from the user-provided reconcile function.
Failure Modes and Common Misunderstandings
-
Treating the reconcile call as event-driven. Writing
if request was a delete event → do Xis a category error. The request contains only{Namespace, Name}; reading “what happened” from the cache is the correct pattern. Symptom: controllers fail to recover after restart, miss deletions when they aren’t running. -
Reading from the API server instead of the cache. Inside a controller, always read from the cached client (the default
client.Clientpassed in). Reading from a raw rest-client bypasses the informer cache and produces hot loops against the API server. Symptom: API server CPU pegged when the controller is running. -
Mutating the cached object directly. The informer cache is shared across reconciles; mutating an object from the cache pollutes other goroutines’ views. The correct pattern:
obj := original.DeepCopy(); obj.Status = ...; r.Status().Update(ctx, obj). Symptom: heisenbugs where status flips unpredictably under concurrent reconciles. -
Returning errors from the reconcile function when retry is undesirable. Returning an error triggers exponential backoff. If the error is terminal (e.g., the desired state is impossible — bad image, invalid configuration), the right pattern is to record the failure on
.status, emit aWarningEvent, and returnnil(orTerminalError). Symptom: the controller spins on a permanently-broken resource, drowning out other work. -
Long-running reconciles. The reconcile function should complete within seconds. Long-running work (waiting on a remote resource to provision) should be implemented as
RequeueAfterpolling, not astime.Sleepinside the reconcile. Symptom: the controller’s worker goroutines starve, queued items pile up, latency for other resources grows. -
Status updates triggering self-reconcile loops. If the reconciler watches the
statussubresource of its own resource type and updates status on every call, every update triggers a new reconcile, which updates status, which… AGenerationChangedPredicatefilters out events where only status changed; the Red Hat operators best-practices guide (redhat.com) discusses this directly. Symptom: 100 % CPU on the controller pod. -
Assuming events arrive in order. Watch events are not strictly ordered with respect to other watches; they may also be batched, deduplicated, or replayed during resync. Reconcilers must not rely on event order — always re-read the current state. Symptom: hard-to-reproduce ordering-dependent bugs.
-
Sharing state between reconcile invocations. A reconciler that caches “I already created Job X for backup Y” in memory will lose that cache on controller restart and create Job X again (or worse, race with itself). The right pattern: the cluster’s API server is the only source of truth; the reconciler is stateless. Symptom: duplicate resources after controller pod restart.
-
Not setting
OwnerReferenceson children. Without owner references, Owner References and Garbage Collection cannot cascade-delete children when the parent is deleted, and orphan resources accumulate. Thectrl.SetControllerReferencehelper is the canonical fix. Symptom: orphan Pods, Jobs, ConfigMaps after parent deletion. -
Adding edge-triggered behavior on top of level-triggered controllers. Trying to retrofit “react to this specific change” logic into a level-triggered reconciler defeats the point. If you need to know what changed, you can read the new state and infer; you should not try to compare against a previously-cached state. Symptom: brittle controllers that fail under restarts, partitions, or replays.
Alternatives and When to Choose Them
- Pure event-driven systems (Kafka consumer, AWS Lambda + EventBridge). Choose for workloads where the event itself is the unit of work and missing it is unrecoverable — payments, audit logs, user-facing notifications. Kubernetes’ level-triggered reconciliation is the wrong tool for these because there is no “desired state” against which to reconcile; the events are the state.
- Saga-pattern orchestration (Saga Pattern System Design). Choose for distributed transactions where each step must execute exactly once and rollback semantics are explicit. Level-triggered reconciliation does not handle exactly-once — it handles eventually-consistent convergence to declared state, which is a different guarantee.
- State machines (AWS Step Functions, Cadence, Temporal). Choose when the workflow has explicit stages with branching, retries, and external waits, and you want a visualizable execution history. Reconcilers are not workflows; they are control loops. The two compose: a Temporal workflow can call a controller; a controller can launch a Temporal workflow.
- CRD + Operator (Operator Pattern). The K8s-native way to extend the control-loop pattern to domain-specific stateful applications. An Operator is a Kubernetes controller for a Custom Resource Definition. Use when you have stateful operational knowledge (database failover, certificate rotation, partition rebalancing) that belongs in code rather than runbooks.
- GitOps controllers (ArgoCD, Flux). Specializations of the control-loop pattern where the desired state lives in Git and the controller reconciles cluster state against Git rather than against an in-cluster spec. Choose for production-grade application delivery.
Production Notes
- kube-controller-manager packages dozens of built-in controllers into a single binary, each running its own watch / reconcile loop. The Deployment, ReplicaSet, Job, CronJob, Node, Endpoint, EndpointSlice, ServiceAccount, Namespace, ResourceQuota, HorizontalPodAutoscaler, GarbageCollector, and PersistentVolumeBinder controllers are all in there. Each is an independent control loop; they communicate only through the API server. See kube-controller-manager.
- cert-manager (cert-manager.io) is one of the canonical example operators. Its
Certificatereconciler watchesCertificateresources, computes whether each certificate needs renewal, and orchestrates issuance against an external CA (Let’s Encrypt, Vault, AWS ACM). The reconcile loop typically requeues every 24 hours to check renewal windows. - Argo CD’s Application controller is an unusual GitOps-flavored controller: it watches
ApplicationCRDs whosespec.sourcepoints to a Git URL, fetches the manifests, computes diff against the cluster, and writes either to a “synced/out-of-sync” status or actually performs the sync. The reconcile period is configurable (default 3 minutes); the GitOps pull model is the level-triggered pattern with Git playing the role of the spec. - The Kubebuilder Book (book.kubebuilder.io) and the Operator SDK (sdk.operatorframework.io) are the canonical tutorials for writing controllers. Both scaffold a project, generate the
Reconcileboilerplate, and provide testing harnesses (envtest) that spin up a local API server for integration tests. - k8s.af (kubernetes failure stories) documents several outages traced to misbehaved controllers — runaway reconcile loops that DOSed the API server, status updates that triggered self-reconciliation storms, edge-cases where finalizers prevented deletion indefinitely. The pattern’s resilience does not absolve the controller author of basic hygiene.
- The Bowes essay’s lineage. James Bowes was at Heptio when he wrote the 2017 essay; Heptio was Joe Beda and Craig McLuckie’s post-Google startup, later acquired by VMware. The essay was effectively an externalization of Heptio’s institutional understanding of the pattern. Joe Beda’s Cloud Native Infrastructure book (with Justin Garrison, 2017) elaborates the level-triggered discussion further.
Historical / Theoretical Lineage
The level-triggered pattern in Kubernetes is not novel — it inherits from several well-established traditions:
- Cybernetics and control theory. The mathematical theory of feedback loops, error signals, and goal-seeking systems originates with Norbert Wiener’s 1948 Cybernetics. The Greek root of “Kubernetes” is the same as “cybernetics” — both from κυβερνήτης (helmsman) — and the etymological resonance is not accidental.
- Digital electronics. Level-triggered vs. edge-triggered are foundational concepts in flip-flop design. A level-triggered latch responds to whether its enable signal is currently high; an edge-triggered flip-flop responds to the moment the signal transitions. The terminology was adopted directly by the K8s designers (per Bowes 2017).
- Google Borg’s declarative reconciliation. Borg pioneered the same pattern at hyperscale — see Borg Omega and Kubernetes Lineage. The Burns et al. 2016 retrospective (queue.acm.org) traces the lineage explicitly.
- Eventual consistency in distributed databases. The CAP theorem and the AP / CP trade-off (see CAP Theorem / BASE Properties) provide the theoretical framework: Kubernetes deliberately chooses AP — the system continues to make progress under partition, accepting that observed state may lag declared state.
See Also
- Kubernetes — the umbrella note
- Desired State vs Observed State — the conceptual dichotomy this pattern operates over
- Declarative vs Imperative Configuration — the user-facing side of the same model
- Kubernetes Object Model — the resource shape on which the pattern operates
- Controller Pattern — the more general concept of which K8s controllers are instances
- Reconcile Function Patterns — idempotency, error-then-requeue, status-update ordering (sibling note, deeper detail)
- Operator Pattern — domain-specific extension of the control-loop pattern
- kube-controller-manager — bundled built-in controllers
- controller-runtime — the Go library scaffolding the pattern
- Kubebuilder / Operator SDK — scaffolding tools
- Watch and Informers — the watch / list-watch mechanism in detail
- Resource Versioning and Optimistic Concurrency — the version token underlying watch
- Owner References and Garbage Collection — controller-managed parent-child cleanup
- Finalizers — controller-driven pre-deletion hooks
- Borg Omega and Kubernetes Lineage — the Borg/Omega ancestors of the pattern
- Kubernetes MOC — umbrella index