# Opinion: K8s IS Data-Oriented Programming. We Just Call It YAML.

# Opinion: K8s IS Data-Oriented Programming. We Just Call It YAML.

Few days back I came across [this](https://www.manning.com/books/data-oriented-programming) excellent book on [Data-Oriented Programming](https://www.reddit.com/r/C_Programming/comments/j90okg/what_is_data_oriented_programming/) which I've since read to a reasonable degree, and as it just so happens from time to time reading about something helps you tie it in with your past experiences where you might've come across the same technology in some shape or form but couldn't recognise it at that point in time...

You can write K8s [controllers](https://k8s.io/docs/concepts/architecture/controller/) for long before you notice that you've been practicing a fairly known, realtviely old programming paradigm. We call our half of it "the K8s way" and leave it there. But the thing you're doing when you author a CRD, keep your controller “thin”, and let etcd hold the truth has a name outside our world: it's *data-oriented programming*, and seeing K8s through that lens explains a surprising amount of why it's shaped the way it is.

## A two-minute detour into what data-oriented actually means

First, a quick disambiguation: this isn't [Mike Acton’s *Data-Oriented Design*](https://neil3d.github.io/assets/img/ecs/DOD-Cpp.pdf), famous in C++ game engines for squezing out hardware performance via cache locality. This is Data-Oriented *Programming* (DOP).

DOP is, at heart, a reaction against one specific habit of classical object-oriented design: welding behavior to data. In classical OOP, an object owns its fields and exposes methods that act on them - the data and the code that touches it travel together, and mutable state hides behind an interface. That bundling is the thing DOP refuses.

It pulls the two apart and rests on a few principles. Separate code from data: logic lives in stateless functions that take data as an argument, not in objects that own it. Represent data with generic, immutable structures - maps, records, plain values - rather than a bespoke class per concept. And keep the data at the center: it's inert, transparent, and the same shape everywhere, so anything can read it without going through a custom API.

The paradigm grew up in the functional, and its whole pitch is reducing complexity. Inert data is easy to reason about. Functions over data are easy to test. Nothing hides.

Now hold that next to K8s.

## The core mapping

**Your spec is data. Full stop.** We call it [YAML](https://en.wikipedia.org/wiki/YAML), but YAML is just the messy, untyped human facade. Once it hits the API server, that YAML is strictly validated against OpenAPI schemas into rigid, structured data. A manifest is inert. It has no methods, no behavior, no opinion about how to make itself real. It's a record - `apiVersion`, `kind`, `metadata`, `spec`, `status` - sitting in etcd as plain serialized data. When you `kubectl apply`, you are not invoking a Deployment's `deploy()` method. You are writing data to a store. The object cannot act on itself, and that's by design (I think).

**Controllers are the behavior, kept rigorously separate.** A controller is a stateless [reconciliation](https://medium.com/@inchararlingappa/kubernetes-reconciliation-loop-74d3f38e382f) loop. Strip away the machinery and it's a function:

```text
reconcile(desired, observed) → actions
```

It reads data it does not own, compares the world it wants to the world it has, and emits the steps to close the gap. The logic lives entirely outside the data it operates on. If that sounds like "stateless functions that take data as an argument," that's because it is exactly that, expressed as a control loop instead of a call stack.

**The data is generic and uniform.** Every resource in the cluster - a pod, a service, your own `PostgresCluster` CRD - shares the same envelope. There is no special-cased class hierarchy of resource types; there is one data shape, parameterized by `kind`. The `spec`/`status` split is itself a small modeling discipline: `spec` is desired state, `status` is observed state, and keeping them separate is what makes the whole system legible. Desired and observed never get conflated into one mutable blob.

[**etcd**](https://etcd.io/) **is the data at the center.** Everything is CRUD over a data store plus watchers reacting to changes. The API server is, architecturally, a typed, validated gateway in front of that data. The control plane is data at rest plus functions that watch it. That is the data-oriented topology, drawn at the scale of a distributed system.

## Isn’t this just a cute analogy ?

The reason the interpretation earns its keep is that the properties we prize in K8s *fall out* of the paradigm rather than being bolted on.

**The "Declarative" Engine:** If you ask 10 engineers to describe K8s, 8-9 will use the word *declarative*.

> Data-oriented programming is the engine that makes that declarative promise actually work.

A YAML manifest simply states a desired reality; the DOP architecture (transparent data sitting in etcd, watched by pure-ish reconciliation functions) is what physically forces a distributed system to match it.

[**Level-triggered reconciliation**](https://hackernoon.com/level-triggering-and-reconciliation-in-kubernetes-1f17fe30333d) is natural once behavior is a pure-ish function of current data. A controller doesn't need a reliable event stream telling it what changed; it can re-derive the right action from the data as it stands right now. Lose an event, restart a process, miss a webhook - it doesn't matter, because the next reconcile reads the data and recomputes. Idempotency stops being a discipline you impose and becomes the default consequence of computing actions from state rather than from history.

[**Composability**](https://en.wikipedia.org/wiki/Composability) comes from the uniform data shape. Because every resource is the same kind of thing, tools that operate on resources - `kubectl`, Argo CD, Kustomize, policy engines, your own scripts - work on all of them without special-casing. Generic data structures buy you generic tooling, exactly as they do in a Clojure codebase where everything is a map.

[**Testability**](https://en.wikipedia.org/wiki/Testability) is the quiet win. A reconcile function is a function of data, so you test it with data fixtures: hand it a desired spec and an observed status, assert on the actions. No live cluster required for the core logic. That's only possible because the behavior was never entangled with the data in the first place.

[Extensibility via CRD](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/)**s** is the cleanest tell of all. When you extend K8s, you don't subclass anything. You declare a new *data shape* and write a function over it. Adding a `kind` is adding a data type, not adding a node to an inheritance tree. That's a data-oriented move to its bones.

## The honest caveats

The analogy is strong, but overselling it would be its own kind of dishonesty, so here's where it strains.

Reconciliation isn't *pure*. The whole job of a controller is to have side effects on the world - start containers, attach volumes, call cloud APIs. The function is pure in how it decides; it is emphatically impure in what it does next. And writing `status` back is a mutation, so the "immutable data" story is really "immutable desired state, mutable observed state”.

And while we disambiguated this from hardware-level Data-Oriented Design earlier, there is one honest bridge to make: the [informer/lister pattern](https://medium.com/@dhruvbhl/informers-listers-workqueues-the-brain-behind-your-controller-f5b0967026de) in `client-go`. Controllers keep a local cached copy of objects and read from that instead of hammering the API server (more on this in another blog post). That *is* data-locality thinking - keep the data you'll touch close and read it cheaply - just operating at the distributed-systems scale rather than the CPU-cache scale.

## What the lens changes for me

Naming the thing you already do is not a trick. Once you see K8s as data-oriented, a few design instincts sharpen.

This lens demystifies [GitOps](https://about.gitlab.com/topics/gitops/). Tools like [ArgoCD](https://argo-cd.readthedocs.io/en/stable/) aren't deployment scripts running commands; they are simply <mark class="bg-yellow-200 dark:bg-yellow-500/30">data synchronizers</mark>. They take the declarative, inert data shapes stored in Git and write them into the etcd database. Viewing it this way makes orchestrating multi-cluster rollouts far more predictable, because you are just replicating static data, not coordinating complex execution pipelines.

Model your CRD's data well, because the data *is* the interface - a clean `spec`/`status` split is doing more work than any clever controller code will, and Keep controllers thin: the inertness of a manifest is exactly what lets you version it in Git, diff it, lint it, validate it, and reason about it without running anything.

You weren't just writing YAML. You were doing data-oriented programming with a very large, very opinionated runtime. It's worth knowing the name of the thing you're good at.
