Naming is a design act
A name compresses a concept's intent into a word, so the act of naming forces you to understand and bound that concept — which makes naming difficulty a direct signal about your design, not just your vocabulary.
You sit down to extract a method. The logic is clear, the tests pass, the only thing left is to name the new function — and you stall. processData? handleStuff? doWork? Every candidate is either a lie or a shrug. Ten minutes later you have a name you don’t believe in and a faint sense that something is wrong with the code, not with you.
Something is wrong with the code. The difficulty wasn’t a vocabulary problem; it was the code telling you that the thing you’re trying to name doesn’t have a single, clear concept behind it. Naming is not the last cosmetic step after the design is done. Naming is the design, surfacing in the only place you can feel it.
After this lesson you can explain why a name is a compression of intent and why writing one forces you to delimit a concept; tell the difference between names that reveal intent (what/why) and names that leak implementation (how); read naming difficulty as design feedback that something does too much; and avoid the opposite failure — names so over-precise they weld a throwaway implementation detail into your public vocabulary.
A name is a compression of intent — it stores what and why, not how. The reader of daysSinceLastLogin knows what the value means and why it exists without reading a single line of the computation. The reader of d knows nothing and must reconstruct the meaning from context every time. The name is the interface to the concept; the body is the implementation. A good name lets a reader skip the body entirely on the 95% of reads where they only need the meaning.
// the name carries the meaning; the body is an implementation detail
const d = Math.floor((Date.now() - user.lastLoginAt) / 86_400_000); // ✗ what is d?
const daysSinceLastLogin = Math.floor((Date.now() - user.lastLoginAt) / DAY_MS); // ✓The terse version isn’t faster to work with — it’s faster to type and slower to read, and reading is where the time goes. The name is the cheapest documentation you will ever write because it travels with the value to every call site for free.
Naming forces you to understand and bound the concept. To name something you must answer “what is the one thing this is?” — and that question is design work, not labeling. The act of choosing chargeExpiredSubscriptions over process forces you to decide: does this function only charge, or does it also email, log, and retry? If it does all four, the honest name is a paragraph, and the paragraph is the design smell. Naming is the cheapest design review you can run, because it runs in your head before you commit.
function process(x: Account[]) { /* ...80 lines... */ } // ✗ name reveals nothing
function chargeExpiredSubscriptions(accounts: Account[]) { } // ✓ name is a contractchargeExpiredSubscriptions is a promise: it charges, it touches expired subscriptions, it takes accounts. If the body later starts also renewing subscriptions, the name now lies — and that lie is a flag that the function grew a second responsibility.
If you can’t name it cleanly, it probably does too much — or you don’t understand it yet. The classic tell is the class you end up calling UserManager, DataHelper, or OrderService because nothing more specific fits. Manager and Helper are not concepts; they are confessions that the box holds whatever didn’t fit elsewhere. A type that resists a precise name resists it because it has no single responsibility — the name can’t be precise because the thing isn’t.
class UserManager {
validateEmail(e: string) {} // validation
hashPassword(p: string) {} // crypto
sendWelcomeEmail(u: User) {} // notifications
recordSignupMetric(u: User) {} // analytics
}You can’t name this better than UserManager because four unrelated responsibilities live inside it; the vague name is the symptom. Split it — EmailValidator, PasswordHasher, WelcomeMailer, SignupMetrics — and each name becomes obvious the instant the box holds one idea. The naming difficulty was the design feedback; the split is what it was pointing at.
Renaming and naming-difficulty are design instruments — but over-precise names are their own failure mode. Treat a stuck name as a probe: when a name won’t settle, ask whether the concept is unclear or oversized, and let the answer drive a split or a rethink. But there is an equal and opposite error: names so specific they bake an implementation detail you will want to change into the public vocabulary.
// over-precise: the name promises a specific algorithm and storage
function fetchUsersFromMySQLWithBTreeIndex(): User[] {} // ✗ welds how into the name
function findActiveUsers(): User[] {} // ✓ names what, hides howfetchUsersFromMySQLWithBTreeIndex reads as precise, but it has nailed MySQL, the index type, and “fetch” into every call site. Move to Postgres or add a cache and either the name lies or you rename across the whole codebase. The skill is to name the stable intent (findActiveUsers — what the caller wants) and keep the volatile how (which database, which index) inside the body where it’s free to change. Precise about meaning; silent about mechanism.
Let naming difficulty drive the design. A teammate wrote this and asked you to help name it — they’d settled on handle:
// what should this be called?
function handle(order: Order): void {
if (order.total > 0 && order.items.length > 0) { // is it valid?
const tax = order.total * 0.2; // compute tax
order.finalTotal = order.total + tax; // mutate the order
db.save(order); // persist
mailer.send(order.customerEmail, "Order confirmed"); // notify
}
}You can’t name it because it does four things: validates, prices, persists, and notifies. handle and processOrder are both shrugs — they name the box, not a concept. The naming difficulty is the design telling you to split. Follow it, and each piece names itself:
function isPlaceable(order: Order): boolean {
return order.total > 0 && order.items.length > 0;
}
function withTax(order: Order): Order {
return { ...order, finalTotal: order.total + taxFor(order.total) };
}
function placeOrder(order: Order): void {
if (!isPlaceable(order)) return;
const priced = withTax(order);
orders.save(priced);
confirmation.send(priced);
}Now no name is hard. isPlaceable, withTax, placeOrder each carry exactly one idea, so each name is obvious — the hard-to-name handle was never a naming problem, it was an undelimited concept wearing a vague label. Note the names say what (placeOrder, withTax) and never how (multiplyByPointTwoAndSave); the tax rate and the mailer are free to change behind them.
▸Why this works
Why is naming difficulty such a reliable design signal, when “I’m just bad at names” feels like the simpler explanation? Because a name is a lossy compression of a concept, and lossy compression only works when there’s a single dominant signal to keep. daysSinceLastLogin compresses cleanly because there’s one idea. UserManager won’t compress because there are four ideas competing for the one slot, and no single word can represent four unrelated things without lying. So a name that refuses to come isn’t usually a vocabulary gap — it’s the concept telling you it has more than one center of gravity. The friction is information. Spend it instead of papering over it with Manager.
▸Common mistake
The seductive opposite mistake: chasing maximal precision and welding how into the name. getUserByIdFromRedisCacheElseMySQL, sortUsersWithQuicksort, EmailValidatorUsingRegex — each reads as rigorous and each is a trap, because it promises a mechanism the caller never asked about and you will eventually change. The caller wants getUser, sortUsers, validateEmail; the cache, the algorithm, and the regex are private decisions. When the implementation changes (and it will), an intent name stays true while a mechanism name becomes a lie or a codebase-wide rename. Name the stable intent; let the volatile detail live and die inside the body. Over-precise is not “extra safe” — it’s premature commitment in disguise.
You're extracting a function and genuinely cannot find a name better than `handleOrderStuff`. By this lesson's lens, what is that difficulty most likely telling you?
A name is a compression of intent — it stores what a thing is and why it exists so readers can skip the how. Writing a good name is therefore design work: it forces you to delimit one concept, and naming difficulty is design feedback, not a vocabulary gap. A name that won’t settle (Manager, Helper, process, handle) usually means the thing does too much or isn’t understood yet — the fix is to split or rethink until each box holds one idea and names itself. The symmetric trap is over-precise names that bolt a volatile implementation detail (FromMySQL, UsingRegex, WithQuicksort) into the public vocabulary; those read as rigorous but become lies the moment the mechanism changes. Name the stable intent, hide the volatile mechanism — and when a name resists you, treat the friction as information about the design, not a failure of your vocabulary.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.