Opinion: K8s IS Data-Oriented Programming. We Just Call It YAML.
Why the "K8s Way" is actually a decades-old functional programming paradigm in disguise.

Search for a command to run...
Why the "K8s Way" is actually a decades-old functional programming paradigm in disguise.

No comments yet. Be the first to comment.
A 502 Bad Gateway tells you that one proxy failed to get a valid response from another system. It does not tell you which layer failed, whether the application ever saw the request, or which timeout a

Cilium eBPF connection tracking, and how SNAT quietly turns stateless traffic into stateful

In Part 1 we got a verified ETCD snapshot back onto an Ansible control host, even with the cluster fully down. I've come across many a blog posts describing ETCD restoration process on a single node,
Part 1: Notes on taking frequent ETCD snapshots, and setting up an automated workflow to getting one back when the control plane is down and kubectl returns nothing but errors.
Documentation discipline done right

Notes From The Platform Layer
17 posts
Notes from the platform layer - distributed systems, production K8s, and the architecture decisions and trade-offs learnt the hard way. Increasingly at intersection of platform engineering and agentic AI: building real agents focused on reducing toil, increasing productivity and faster turnaround times.
Few days back I came across this excellent book on 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 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.
First, a quick disambiguation: this isn't Mike Acton’s Data-Oriented Design, 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.
Your spec is data. Full stop. We call it 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 loop. Strip away the machinery and it's a function:
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 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.
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 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 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 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 CRDs 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 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 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.
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. Tools like ArgoCD aren't deployment scripts running commands; they are simply data synchronizers. 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.