Conventions on writing code that reads like English prose. Use when writing or modifying code in object-oriented codebases and/or languages.
Code is read far more than it is written. Every method body is a paragraph, every variable name is a noun or predicate, every call site is a sentence. Write code that a domain expert can read aloud and understand without deciphering technical notation.
A method body tells a story from top to bottom. Each statement is a sentence; each block is a paragraph. A reader should follow the logic without backtracking.
val userIsMoving = speed.value > SPEED_LIMIT_METER_PER_SECOND
val coordinatesIsInaccurate = coordinates.accuracyInMeters > ACCURACY_LIMIT_METER
if (userIsMoving || coordinatesIsInaccurate) return
val cached = CachingMoments(moments)
val currentTimestamp = LiteralTimestamp(clock.now())
val last24h = (currentTimestamp - 24.hours)..currentTimestamp
val todaysMoments = TimeRangedMoments(last24h, cached)
val beenHereRecently = todaysMoments.any { it.place.id == place.id }
if (beenHereRecently) return
val moment = moments.new()
moment.update(currentTimestamp)
moment.update(isNotable = false)
moment.update(place)
moment.commit()
Every boolean variable or method reads as a natural language assertion that is either true or false.
is / has / was + adjective or state — userIsMoving, coordinatesIsInaccurate, isEditCancelled, isSentimentOverridden.beenHereRecently, updatesMade().is + adjective or has + noun — isNewlyCreated(), isAlertNecessary().isValid over isNotInvalid; invert at the call site with !isValid.Choose words that belong to the problem domain, not the solution domain. A reader should feel they are reading about the real world, not about data structures.
| Prefer (domain) | Avoid (technical) | Why |
|---|---|---|
forget() | delete() | People forget moments; databases delete rows |
Moment | Entry | The domain is journaling, not data entry |
Memorable | Linkable | Describes what it means to the user |
sink(event) | handle(event) | An event sink sinks events; "handle" is generic |
NullIsland | NoPlace | A real cartographic concept for 0,0 coordinates |
coordinates | coords | Full words, no abbreviations |
When naming, ask: "Would a domain expert use this word in conversation?" If the answer is no, find the word they would use.
Use language features to make expressions read as close to English as the language allows.
currentTimestamp - 24.hours, event["name"]. Arithmetic and indexing read naturally when the semantics match.it.sentiment in sentimentRange, it.timestamp in timeRange. "X in range" reads as an English predicate.24.hours, 7.days, 3.hours. Numeric literals with unit suffixes read as quantities.EditableMoment by origin. "X by Y" reads as "X backed by Y."Decor { ... }, UseCase { ... }. A single-method interface invoked as a lambda reads as a verb.At call sites, use named arguments to make construction and invocation self-documenting. Each argument label reads as a clause in a sentence.
CountLimitingMoments(
limit = 10,
origin = OrderRandomizingMoments(
origin = VicinityMoments(
coordinates = coordinates,
distanceLimitInM = 100.0,
origin = notableMoments
)
)
)
Named arguments are especially important for:
update(isNotable = false) instead of update(false).distanceLimitInM = 100.0 instead of a bare 100.0.origin = label clarifies the wrapping chain.Use cases, commands, and actions are named as imperative verb phrases, optionally with articles. The name reads as an instruction to the system.
Multiple overloads of the same method name accepting different argument types create a narrative mutation sequence. Each call reads as a step in a story.
moment.update(currentTimestamp)
moment.update(isNotable = false)
moment.update(place)
moment.commit()
This reads as: "Update the moment's timestamp. Mark it as not notable. Set its place. Commit."
when expressions (or switch/match in other languages) read as English decision lists. Keep each branch to a single expression or delegation — multi-line branches break the tabular rhythm.
when (event) {
is TextInputEvent -> handleTextInput(event)
is SelectionEvent -> handleSelection(event)
is RefreshRequestEvent -> present()
is CancellationEvent -> handleCancellation(event)
}
Error messages in precondition checks (require, check, assert) are full English sentences with punctuation. They describe the violated contract, not an implementation detail.
"Sentiment value must be between 0.0 and 1.0; given was $value.""invalid sentiment" or "value out of range"When constructing object graphs through nested decoration and named arguments, the result reads as a declarative specification — a description of what something is, not imperative steps for how to build it. The application root or dependency graph should read like a specification document.
coordinates not coords, timestamp not ts, configuration not config.// this does X comments to explain what code does — rename so the code says it itself.MomentEditor.