← Back to blog

Names that evolve: better code through naming and fixing linguistic antipatterns

A GetUser that creates records and an IsValid that returns an integer: six stages of naming and four linguistic antipatterns, with C# examples of how to fix each one.

Cover image for the i4B article Writing better code through naming and fixing linguistic antipatternsThree men brainstorming in a room with a whiteboard and graffiti art on the walls.

A method called GetUser creates a database record. A variable called data holds the identity of the person changing an order. A method called PrepareFilters quietly fills in missing dates. Each name leaves the reader with an incomplete or misleading picture of the code. Names help developers decide what to call, what to change and where to look for a bug, and when every one of those decisions requires opening the implementation, the vocabulary of the codebase is doing too little work.

Two ideas are particularly useful here: letting names evolve as understanding grows, and watching for recurring mismatches between what a name suggests and what the code actually does. This article walks through both, with C# examples taken from flight booking, discounts and order workflows.

Naming as a process: six stages from nonsense to domain concept

The starting point is Mark Pearl's post Stages of Naming [1]. Pearl credits J. B. Rainsberger's earlier four-stage model [2] and Arlo Belshee's expanded six-stage version. His central observation is that useful names emerge while developers learn what the code means. Naming and design influence each other: a better name often appears only after the code underneath it has been restructured.

Table 1. Six stages of naming and what each stage communicates

StageWhat the name communicates
NonsenseA temporary label without useful meaning.
HonestSomething the code really does, although the description may be incomplete.
Honest and completeAll relevant behaviour, often exposed in an awkwardly long name.
Does the right thingA coherent responsibility after structural refactoring.
IntentThe purpose of the operation.
Domain abstractionA concept the team understands in the language of the business.
Six stages of naming shown as arrows with example method names: preload() as nonsense, DoSomethingEvilToDatabase() as honest, ParseXmlAndStoreFlightToDatabaseAndLocalCacheAndStartProcessing() as honest and complete, StoreFlightToDatabaseAndStartProcessing() as does the right thing, BeginTrackingFlight() as intent, and MonitoringPanel.Add(new Flight()) as domain abstraction
Figure 1. Naming evolution illustrated with a flight-tracking method. Based on the six-stage model described by Mark Pearl [1].

The important transition is from describing tangled behaviour to restructuring it.

A shorter name becomes useful when the code behind it has a clearer responsibility. Merely shortening an accurate but uncomfortable name hides the problem again, and the next reader has to rediscover what the long name had already recorded.

An example: discovering what a booking operation really does

Imagine working on flight booking code. An initial Process method is a placeholder. Reading it reveals that it stores a flight, reserves seats and sends a confirmation. Calling it StoreFlight would be honest about only part of its behaviour. An intentionally complete intermediate name makes the problem visible:

void StoreFlightReserveSeatsAndSendConfirmation(BookingRequest request)

That name is useful during discovery, because it records knowledge that would otherwise stay in one developer's head. It also prompts questions. Do these operations belong together? Do they fail independently? Which component owns each responsibility? After separating the underlying operations, the application workflow might read:

BookingId ConfirmBooking(BookingRequest request)
{
    var booking = CreateBooking(request);
    ReserveSeats(booking);
    StoreBooking(booking);
    SendBookingConfirmation(booking);
    return booking.Id;
}

This sketch illustrates vocabulary and decomposition; transaction boundaries and delivery guarantees need their own design. ConfirmBooking is appropriate only if confirming a booking has exactly this meaning in the domain. A domain term earns its brevity through a shared definition.

THE RULE IN ONE LINE

A long name is a design signal, not proof of a single-responsibility violation. A workflow can legitimately coordinate several steps; the question is whether it represents one coherent purpose and whether its collaborators have clear responsibilities.

Why provisional names pay off

Treating names as provisional reduces the pressure to invent the perfect term before the problem is understood. During local exploration, a placeholder keeps the work moving. Before sharing the change, replace it with the clearest name supported by what you have learned.

Honest intermediate names also preserve discoveries. If reading a method reveals an unexpected database write, recording that fact in its name makes the next review better informed. The surprise becomes visible at the call site instead of hiding three levels down.

Naming discussions can also reveal missing concepts. In one project we needed vocabulary for customer-specific discount thresholds across monthly, quarterly, half-yearly and yearly arrangements. We considered DiscountStep, then a term based on a price cap, before arriving at DiscountMilestone. The milestone metaphor suggested routes for the different periods and a map for each customer.

A short naming discussion aligns the team's mental models before the differences get baked into classes and API contracts.

The benefit was a shared way to reason about the feature, the same kind of alignment between people that we describe in Enabling Team: how Agile is it? Keep such metaphors only while they explain the business accurately, and replace them when they start forcing awkward interpretations.

Linguistic antipatterns: four broken promises to catch in code review

The term comes from work by Venera Arnaoudova and her collaborators. Their study, Linguistic antipatterns: what they are and how developers perceive them [3], examines recurring inconsistencies among names, types, documentation and implementations, using examples from real open-source projects. The central problem is a broken expectation: the code does more than its name suggests, delivers less than it promises, or communicates conflicting meanings.

Table 2. Four linguistic antipatterns and how to fix them

AntipatternThe broken promiseFix
Getter that writesGetUser creates a user when none existsGetOrCreateUser, or separate lookup from creation
Predicate without yes/noIsValid returns an integer statusbool IsValid or GetValidationStatus
Generic nameauditData holds the current user making the changecurrentUserContext
Hidden business effectApplyToCart marks backorder items so they stop being suggestedMarkBackorderItemsAsAddedToCart

1. A getter that secretly creates data

User GetUser(UserId id)
{
    return repository.Find(id) ?? repository.Create(id);
}

A caller checking whether a user exists may accidentally create one. The hidden write changes how you must reason about retries and permissions. If the combined behaviour is intentional, call it GetOrCreateUser. Otherwise, separate lookup from creation and make absence explicit in the lookup's return type.

2. A predicate that does not answer a yes/no question

A method named IsValid suggests a Boolean result. Returning an integer status forces callers to discover and interpret an unexpected contract. Use bool IsValid(...) for a predicate, or ValidationStatus GetValidationStatus(...) when several outcomes matter. If validation deliberately throws, a name such as ThrowIfInvalid makes that behaviour clear. Whichever you choose, follow the conventions of the surrounding API consistently.

3. A generic name that hides a specific meaning

In one codebase, auditData held information about the current user performing a modification. The name left readers guessing what kind of audit information it contained; currentUserContext describes the role directly. Generic names still make sense in generic algorithms, but business code usually has more useful vocabulary available.

4. An operation that omits its business effect

ApplyToCart appeared in a backorder workflow. It marked entries as already ordered or added to the cart so they would no longer be suggested to the customer, and the name concealed exactly the state change that mattered. MarkBackorderItemsAsAddedToCart communicates that effect, provided that “added” is the actual transition. Agreeing whether the business means “added” or “ordered” is part of finding the right name.

Making naming review part of everyday development

Review names whenever an implementation change exposes new knowledge. Read important calls without opening their bodies and check whether the expected behaviour matches what actually happens. Clear names matter just as much when part of the reading is done by tools: an AI coding agent, like a new team member, can only infer intent from the vocabulary it finds, as we discuss in AI agents on mature codebases.

Where broken promises hide most often

Pay particular attention to writes hidden inside queries, to validation outcomes that are not really yes or no, to policies for missing values, and to collection cardinality, where a singular name returns many items or a plural one returns at most one.

When an accurate name becomes unwieldy, inspect the responsibility before editing the wording. When teammates interpret a term differently, hold a short discussion and record the agreed meaning in the project's vocabulary.

Update names, comments, tests and callers together so the code tells one consistent story. For public APIs or serialized fields, plan for compatibility before a rename, because the old name may live on in clients and stored data you do not control.

Names that keep up with understanding

A useful name captures what the team currently understands about the code. As that understanding develops, the name should develop with it. Sometimes the improvement is a clearer identifier; sometimes it requires separating responsibilities or discovering a missing domain concept.

Linguistic antipatterns provide practical warning signs along the way: promises the code does not keep, behaviour the name conceals, and words that force readers to guess. Preserve what you learn in the code's vocabulary, and let that vocabulary guide the next design decision.

Try it on your next pull request: pick three method calls, read only their names and write down what you expect each one to do. Then open the bodies. Every surprise is a name worth fixing. If you are bringing AI coding agents into an older codebase, see how we prepare one in AI agents on mature codebases.

FAQ

What are the stages of naming in software development?

A six-stage model popularised by Arlo Belshee and described by Mark Pearl moves from nonsense (a placeholder such as preload) through honest, honest and complete, does the right thing and intent, to domain abstraction. The key step is the middle one: an honest but awkwardly long name exposes tangled behaviour, and the code is restructured before the name gets shorter.

What is a linguistic antipattern in code?

A linguistic antipattern is a recurring inconsistency between a name and what the code does, described by Venera Arnaoudova and colleagues in Empirical Software Engineering in 2016. Typical examples are a getter that creates records, an IsValid method that returns an integer, or a method whose name omits the state change it makes. Each one breaks the reader's expectation.

Is a long method name a code smell?

Not by itself. A long, honest name such as StoreFlightReserveSeatsAndSendConfirmation is a useful design signal: it records everything the method does and prompts the question of whether those steps belong together. If they form one coherent purpose, a shorter domain name like ConfirmBooking is earned. If not, split the responsibilities first, then rename.

Should a method called GetUser ever create a user?

Only if its name says so. A getter that silently writes to the database surprises callers and complicates retries and permission checks. If the combined behaviour is intended, name it GetOrCreateUser. Otherwise keep lookup and creation separate, and make a missing user explicit in the lookup's return type, for example a nullable or optional result.

How do you review naming during code review?

Read important calls without opening their bodies and predict what each one does, then compare with the implementation. Look especially for writes hidden in queries, predicates that do not return Booleans, generic names such as data, unclear policies for missing values and wrong singular or plural forms. Rename together with comments, tests and callers.

Sources and assumptions

Code examples are simplified C# sketches. The discount, audit and backorder examples come from i4B client projects and have been reduced to the naming problem they illustrate.

  1. Pearl, M. (2015). Stages of Naming. Blog post describing the four-stage and six-stage naming models.
  2. Rainsberger, J. B. The Four Elements of Simple Design. Blog post, source of the four-stage model cited in [1].
  3. Arnaoudova, V., Di Penta, M. and Antoniol, G. (2016). Linguistic antipatterns: what they are and how developers perceive them. Empirical Software Engineering, 21(1), 104–158.

Key takeaway

Treat names as provisional: they record what the team currently understands about the code. When an honest name grows awkwardly long, such as StoreFlightReserveSeatsAndSendConfirmation, restructure the responsibility before shortening the word. In code review, look for four linguistic antipatterns: getters that write, predicates that do not return Booleans, generic names like data, and operations that hide their business effect.

← Back to blog
Turn these insights into real results.
contact us
Smiling man with a beard, wearing a yellow corduroy shirt over a teal t-shirt.

Discover our software hub and test working demos

Explore