Koleman Nix

How to name things

Naming is one of the most fundamental skills in software engineering, and in knowledge work generally. Clear language almost always entails clear thinking. When someone’s language is vague, it’s relatively likely that there is lacking internal clarity.

Every time we name something, we are communicating: a local variable, a function, a field on a struct, an API endpoint, a page, a database table and its columns, the product itself. Many names, like url parameters, are necessarily both user- and code-facing, and these constraints matter. We communicate with our colleagues, with our future selves, and with language models through this text, through names. Language is where we encode our ideas; the unreasonable effectiveness of the language model proves this.

While we’re talking about LLMs: when we point a model at a fresh, hand-written, highly focused codebase, the initial results are often stellar. Crystal clear additions, subtle understanding displayed. A bit later on, the same model in the same codebase looks like a seal in a tar pit. This is often perceived as the model getting worse, but what’s actually happening is that the codebase, the names and terms that make up the model’s entire context, is being slop-fried, like a JPG, to borrow a term from Victor Taelin.

I think of it less as lossy compression, though, but moreso as lossy expansion or interpolation. Given a clear plan, anchored in real understanding, a model can interpolate outward and produce something still solid. But if we keep interpolating from the interpolations, asking the model to “enhance” what is already an approximation, we eventually cross into fully fictionalized territory, having strayed too far from the core of intent and clarity established at the beginning. This is why the sustainability of working with models in a codebase depends so heavily on your willingness to reduce, clarify, and hone your terminology. And honing terminology turns out to be the same activity as honing the complexity of the system itself.

Don’t-repeat-yourself, understood properly, says that a fact should be expressed in exactly one place, and that if two pieces of code express the same fact, they must point at the same place. This is just a compression header; a common facts table. But unlike a huffman code, we’re more interested in semantic compression than bit-identical compression. Two values that happen to be equal are not always the same fact. You might have two global constants that are both the number 64 and mean entirely different things where they are used. Those should be two globals. We are not trying to abstract the concept of 64, but rather the facts that the max name length and the default queue depth are 64, and they match accidentally rather than essentially.

The craving for consistency

Holding onto this framing of naming as communication, let me address something I’m asked about often by less experienced engineers. There is a craving for consistency, as though consistency were a virtue in itself. The appeal is the same as the allure of a Diet with a Name or an endorsed Workout Plan (my mother-in-law recently informed me that she is Protein Pacing, capitalized, which upon further inspection simply means eating protein). The desire to offload the decision to a Rule or a System. It’s a form of refusal to engage with the primitives, a dread of re-inventing the wheel (reinvention is just learning), the same mindset that leads to cargo-culting. A refusal to go deeper. So the question arrives as a demand for a rule. Any time a value is optional, do I name it maybe_thing? thing_opt? Or do I leave the optionality out of the name entirely?

The answer is to buckle up and engage deeply; to read the code from the top, in your head, pretending to be your teammate. Then ruminate on this variable and its properties. Is it always optional? Is it optional only because its absence is an error, in which case perhaps we should handle that error earlier, or name the variable in a way that says so? Is it a fallback? Is it a set of overrides? Overrides are expected to be optional; they layer on top of a baseline configuration, and that expectation is part of what they are.

What you should be consistent about is the practice of thinking along these axes. The decision of what to name something should take in as many inputs as you can gather, not just the type of the thing, and not even just its current usage. Look around, consider essence, and also consider symmetry: is there a dual or a triple of this concept that could be named to mirror it, so that the names visibly refer to one another on the page, like src and dst?

Notice the kind of advice this is. “Never abbreviate” and “prefer shorter names” are terrible rules; they’re the offloading kind, attempts to get a good result without engaging. “Consider the symmetry of these concepts, consider lining things up” is advice of a different kind. It asks you to put yourself in the shoes of whoever reads this next, person or model, and to treat the name as an opportunity to hand them what you were thinking. Naming well is an act of intellectual empathy. Your caller didn’t implement the function and shouldn’t have to; don’t give it a name that leaks the internals. Name it what it does.

created_at, uploaded_at

Let’s have more examples. All programming advice sounds fine in the abstract, hence the proliferation of garbage. Consider a database column holding the date on which a thing began to exist.

created_at is a reasonable default name for this. The at suffix is doing the work of a preposition of time; at can also mean place, but if we use it for timestamps and hold to that, it’s good enough. You could argue for created_on when the column holds a date rather than a timestamp, since we say “on Christmas Day” but “at 7:59 p.m.”. But let’s take created_at as a good enough default.

If created_at is such a good default, why shouldn’t every table simply have one? Why do I sometimes name things uploaded_at or issued_at? It comes down to what the row means and is, as well as the column. Some rows model synthetic objects, things that exist only inside the software system, for example a ‘display config’ record that talks about where stuff goes in the ui. Such a config object is a synthetic implementation artifact, not part of the domain of the system, really it was created at the moment its row was created, so created_at is just fine. But some rows represent things that exist in the real world. Suppose the row represents a document we received from a user. I don’t want to claim the document was created in 2023 when 2023 is merely the year it reached our system. It may matter a great deal that the document was created in 1995, because it is someone’s birth certificate, and uploaded on June 7th, 2023. Consider a system that cares very much about when evidence was received, as well as the date of the evidence itself. So we think about what the thing is, and we imagine someone reading all of the date columns on this table together (uploaded_at, record_date (a date, not a timestamp), and last_reviewed_at), and we ask how to name them so that, taken as a set, they tell one clear story.

The same rumination applies to the shape of a value, not just its name. Take the fallback config from earlier: is a fallbackConfig a sparse Map of overrides, or a total, fully parsed config object? If it ends up an Option[Map[String, String]], missingness is being expressed twice: any given key can be absent from the map, and the map itself can also be absent. Is the difference between no map at all and an empty map meaningful? Maybe. Perhaps None marks an error path, a config file that failed to parse, and deserves to be handled loudly and early rather than carried around. But if the two cases mean the same thing everywhere they’re read, the Option is noise, and it should be folded into an empty map at the boundary. What began as a question about a name turned out to be a question about the model; the two are rarely far apart.

Names can hide generality just as well as they hide vagueness. I once came across this signature:

private def mergeTargetValuesIntoSourceTemplate(
    template: Map[String, String],
    values: Map[String, String]
): Map[String, String]

Inside was a hand-rolled implementation of template ++ values. This is a case study in terrible naming. The programmer failed to notice the generality of the operation, likely because the operation was named after its need rather than according to its essence, and so it could only ever be used by its need. Its essence was just map union, which is idiomatically available under ma ++ mb. Nobody looking for a way to combine two maps will find that function, or trust it, under that name, and nobody reading the call to that function should trust its name; they’re probably here because there’s a bug, and need to know what it does. If it were simply replaced with the ++ idiom, the whole exercise disappears.

As go the names

Most of the text in a codebase is usages of your own names. Whether you are a human or a language model, this is your entrypoint to semantics. Replace every name with randomized identifiers, and while behavior could still be recovered, it would be a lot more work, and much intent would be lost. Often, when working on a system, I’ll encounter an uncomfortable name: sometimes it’s a verb, an operation, sometimes it’s a noun, a type, but it makes no difference. I’ll go investigating; it’s important that I solidify my understanding of this [thing] in order to do [my task] correctly. Usually, I do not find a poor label on a clear concept, but instead a best-effort label on a vague concept, and often conflicting semantics. Both readings are defensible given the name, but one of them is a bug. Say our document system has a field called documentDate. This seems like a fine name until you realize it says nothing about origin, intent, or semantics. The review screen displays it as the date on the document, the record date of that birth certificate. The ingestion path, written a year earlier, populates it with the day the file arrived. And somewhere in between, a staleness rule rejects bank statements whose documentDate is more than 90 days old; whichever meaning its author had in mind, it’s checking the wrong one for some of the documents in the system. And now we can’t even correct the name without a database migration, a heuristic backfill, and a lot of risk and extra work. We sadly add some // WARNING comments around our discovery, and carry on. This is why demanding clarity from names is so important. Not for its own sake, though it has significant merit, but because it so often leads to real corrections of behavior. Tugging on a bad name is like tugging on a piece of thread that leads you to a knotted mess.

The same technique is useful to apply in the other direction, before there’s much code to read at all. It can, and should, be difficult to name incomplete things whose design has not yet crystallized. The name will follow from the discernment that happens during creation. If the design is nearly solidified and we still can’t name the thing, that’s a decent signal that something might be wrong. And when a great name emerges on its own partway through refinement, that’s a high-quality signal.


So, how should you name things?