Pattern your code to keep it clean
Great code has a rhythm. It has a style. It has a pattern.
There is very little noise in great code. You can understand the behavior the code describes readily. You have to think very little about the way the code is organized, the way the behavior is expressed, or where to look for what you’re looking for.
Good organization doesn’t necessarily mean the code is performant, bug-free, or well-designed for the change at hand… yet poorly organized code often guarantees all three. In fact, most of the complaints I’ve ever heard about legacy codebases were not so much in the behavioral problems, but rather in the organizational problems.
Of course, this doesn’t mean it’s secretly easy to work with legacy code. Poor organization matters a great deal, and it’s harrowing to work your way out of. (And thus, the succubus of full-rewrites often finds her entrance.)
It’s much better to plan for good organization early, so that your codebase forestalls ever becoming a legacy.
And thus, the importance of a pattern.
This article will describe a very specific pattern that’s served me well in my career. We’ll use this example to surface both the benefits of using patterns, as well as the maturity required to use them for maximum benefit. Then we’ll circle back and discuss The Rule of Patterns to finish things up.
After all, every great engineer has a rolodex of patterns to draw from, a visual language with which to compose great code. And it’s only through all of us experimenting and sharing that our shared pool becomes increasingly robust and effective.
Let’s dive in!
The File Structure Pattern
One great way to reduce the amount of noise in a codebase is to put things where you’ll expect to find them in the future. But the question becomes—where will you expect to find them?
This question arises on many levels—from function definitions to breaking up your code between repos. And if there were one clear pattern to apply across every level, that’s what we’d be discussing. However, scale changes things. And today we’re only approaching the question with regards to how to organize a single code file.
So… how do you organize a file full of code?
Is your gut always right?
Some developers, even truly great ones, prefer to organize their files intuitively. Take Kent Beck, from Tidy First?:
Reorder the code in the file in the order in which a reader (remember, there are many readers for each writer) would prefer to encounter it.
You’re a reader. You just read it. So you know.
However, there are a few problems with this approach…
1. You are not your reader
I’ve been programming since I was 10. I’m 40 now. (Yes, flabbergasting to think any coder codes for that long.) And yet, I find code from just a year ago surprisingly different from what I would write today. I suspect you feel the same.
It means we’re learning, growing, and hopefully becoming better. But it also means that even in the best of cases, the you writing code today can never be the you that reads it later. And you certainly can never be the junior devs on your team. Or the devs hired five years after you leave to fix your legacy codebase.
2. There are many readers
Kent says this himself. Readers far outnumber writers. There are devs of every experience levels, devs from different coding generations, devs from teams who need to interface with your software, QA engineers, technical product managers, prospective contributors to your open source codebase, and of course, your many future selves.
Each reader brings different expectations to your codebase, and will not necessarily have the same intuitions as you. That may be because they’re stupid—but probably not. When in doubt, I find it’s better to assume I’m to blame. That I simply hadn’t made my intentions clear enough to my intended readership.
3. You are not Kent Beck
You probably don’t have his experience, and you almost certainly haven’t devoted a large portion of your career to discovering good patterns and elucidating them. You have no reason to expect “So you know” to work for you.
Now, perhaps you’ve truly dedicated yourself to your craft, and perhaps your intuitions are as sharp as vorpal swords. Even if this is the case, it’s better not to think so highly of yourself. Among other things, humility ensures you’re always open to improvement. And so even for the 0.1% of devs operating at the highest level, it’s better to assume your intuitions aren’t foolproof.
If not gut, then what?
If you can’t trust your gut, then what can you trust? A pattern.
The entirety of the modern world depends upon a robust set of patterns that ensure the failures of intuition do not result in the failure of our civilization. The name of these patterns? The Scientific Method.
The scientific method is not always the most efficient path to knowledge, and in practice, it is frequently corrupted. And yet, the mere existence of these conventions ensures that the knowledge developed through their application is at least 51% correct… and probably much greater.
One of the scientific method’s chief benefits is that, though it isn’t perfect, every scientist across the entire planet comes to a new paper with the same set of expectations. They know how to scan a document with ease. They understand the implicit tradeoffs of a “randomized controlled trial” or using statistical significance versus statistical power because they’ve seen these patterns many times before.
So rather than hoping your intuitions as a coder align with the intuitions of your readers—train your readers with consistent, clear, frequently-applied patterns. This way, after having seen the pattern two or three times in your codebase, they’ll quickly be able to align their “intuition” to your “intuition,” because it’s coded as a pattern, everywhere you look.
The pattern itself
99% of the code files I write look like the following:
// import
import * as utils from 'utils';
// types
type Result = {x: number; y: number};
// vars
const TIMEOUT = 1000 * 5;
// fns
const helpDoThing = (x, y) => true;
// export
export const doTheThing = (): Result => {};
You’ll notice immediately this isn’t much different from how you normally organize your code. (And that’s a good thing! It means this pattern models reality somewhat well.)
There are likely two differences to how you normally write code:
- Strict adherence to this order
- Comments as landmarks
The order of declarations
The pattern requires that you never deviate from the order of declarations shown above. Imports are all always first. Exports are all always last. Helper functions always occur after variable declarations.
Why?
Since every file is organized in the same way, a reader of any file will immediately know where to look what they’re looking for. And they’ll also know where to add new code so that the next reader derives the same benefits.
This reduces the amount of noise considerably.
Do you want to understand the data structures of this module? Visit the // types
section. Do you want to understand the internal state of this module? Visit the //vars
sections.
Co-location, much?
A savvy engineer will likely start to worry about co-location. After all, shouldn’t code that changes together be organized together? Does Mike think the co-location rule doesn’t apply at the file level? Isn’t this good code…
const DEFAULT_WAIT = 1000 * 60;
const wait = async (wait: DEFAULT_WAIT) => {
return new Promise((done) => setTimeout(done, wait));
};
(Let’s table the good code question for a moment.)
First, co-location is a great pattern. You should use it as often as it applies. And second, co-location works in tandem with the file structure pattern.
Over time, code files tend to grow. That growth typically includes adding behavior that doesn’t directly relate to other things happening in the file. Things get muddled, and great engineers realize it’s time to refactor.
A time to refactor
The strict organization of declarations makes it very clear, even to junior engineers, that too many things are going on in the same file. How?
Once it becomes a burden to scroll between sections in a strictly ordered file, you know the file is doing too many things. After all, if all of the // types
, // vars
, and // fns
were directly related to the file’s // export
, you wouldn’t feel as though you lost context when reading from top to bottom.
In contrast, an intuitively organized file can contain multitudes. This may seem like a benefit—that you could write an entire program in a single file—until you realize every programming language invented modules for a reason.
Great code has clean boundaries between modules, and great modules only export the interfaces other agents should have access to. Intuitive organization supports a style of programming where disparate agents can access protected state and logic that should otherwise be hidden away.
So if we refer back to our sample co-located example:
const DEFAULT_WAIT = 1000 * 60;
const wait = async (wait: DEFAULT_WAIT) => {
return new Promise((done) => setTimeout(done, wait));
};
Sure, this code is great in isolation. But when you pack 900 lines of this stuff in a single file, you’d better believe DEFAULT_WAIT
will inevitably be used somewhere you hadn’t intended.
Much better to start here:
// vars
const DEFAULT_WAIT = 1000 * 60;
// export
export const wait = async (wait: DEFAULT_WAIT) => {
return new Promise((done) => setTimeout(done, wait));
};
Ask yourself, what additional methods would it be logical to add to this file? What methods would it be okay to share state with? Perhaps delay-related functions like randomWait()
?
But will it scale?
Are you convinced that a “trigger” for refactoring is a good idea? Or are you worried that it might lead you to write unreasonably short files?
In my experience, the strict format seems to “break” somewhere around 300 lines of Typescript. It’s considerably longer in Go and a bit shorter in Python. But that said, it doesn’t “break” in every file this way. My codebases have many files that get up to 1,000 lines and are very readable, because the content is all built around a single idea.
In the past, I’d use linters to scream at me when I passed a certain threshold, but I found they could offer only a gross approximation. The strict order asserts the true value.
Comments as landmarks
Comments should only be used when the code cannot speak for itself.
As such, good code tends to reduce the need for good comments. The preeminent exceptions are “hacks,” when you have to do something that seems bad for a reason the reader wouldn’t expect. And “documentation,” when you’re writing library code. Almost everything else is better written into your code itself, whether as good naming or as tests.
But there’s a third exception: Comments are an excellent way to increase the readability of your code. This is the sense in which the strict format uses comments. Contrary to what you may expect // import
, // fns
, and // export
should actually appear in your code.
Why?
Whitespace improves readability
Compare the default Google Doc to any given Medium article. Which is more readable? Very likely, you said Medium. And why? It’s the whitespace.
Studies have repeatedly shown that reading comprehension is heavily impacted by the presentation of ideas in text. Font choice, font size, print quality, and spacing each reduce reading comprehension in the 20%-40% range. And yet most people are oblivious to the impact—as evidenced by the sheer number of unreadable websites out there.
The same thing goes for code. By including these mandatory “landmark” comments in your code, you make it substantially easier for readers to understand the underlying ideas.
Landmarks improve navigation
Similarly, having a consistent set of “landmark” comments makes it very easy to locate what you’re looking for.
First, scrolling becomes much more effective. The brain is a pattern-recognition machine. Show it the same series of pixels a thousand times, and it will become very good at grabbing exactly what it’s looking for as you scroll a document. In contrast, using varied nomenclature (or no comments whatsoever) deny your brain the ability to optimize. Instead, the signal is buried in the noise.
Similarly, using the same landmarks over and over ensure that no matter what file you’re in, you’ll be able to leverage the same “jump to” keyboard commands. No matter which file you’re in, out of thousands in a code base, literally only one line will begin // types
. This makes it very, very easy for you to get around.
Rhythm and beats
In the auditory realm, a simple beat can transform noise into music. The beat orders our experience of the world into discrete chunks, and those chunks are easier for us to categorize.
Writing is the same. Great writing has a rhythm of beats that the writer leverages to reinforce his message. Shakespeare would break his iambic pentameter at precisely the moment of greatest tension, when he wanted the audience to take notice.
Landmarks are beats. They convey a sense of order that clarifies the intent of the file. A landmark’s presence or absence says something. The space between landmarks says something. And the knowledge conveyed is all the more important because the reader knows what to expect in each section.
Take some examples: If a long file has no // types
, but a long list of // exports
, what would you be concerned about? Similarly, if a file is just // vars
, what do you think is the purpose of that file?
Variations on the pattern
You may be wondering how this pattern applies to files that don’t match the sample above. But what about object oriented programming? What about scripts? What about frontend component frameworks?
Here’s the full set of landmarks. They always, always, always occur in this order, though many will be left out, depending on what kind of file we’re in.
// import
// types
// vars
// fns
// models <-- Zod, superstruct, valibot, etc.
// classes <-- including custom errors
// config <-- for instantiating SDKs
// hooks
// components
// styles <-- remember css-in-js?
// export
// run <-- for scripts
You could imagine a file router’s file route to look like the following:
// import
import type {LoaderFn, RouteComponent} from 'redish';
// config
export const loader: LoaderFn = () => {};
// components
export default Route: RouteComponent = () => {};
What’s important isn’t the specific set of 12 landmarks I use, but rather that you develop a consistent, repeated set of landmarks in your own codebases. After all, it doesn’t matter if your codebase resembles mine. Your readers may never read my code!
Instead, the goal is to provide the readers of your code with a pattern they can immediately latch onto in order to reduce the noise inherent to code as a medium. The fewer flags, the better.
Breaking the pattern
I began this exploration by asserting that 99% of my code files follow this pattern. Well, what about the other 1%?
The truth is that not every file you ever write will be a good fit for the strict format. The world is a messy place, and the wise know that no rule applies to 100% of cases.
I find that the strict format ceases to be useful in files where most of the declarations would occur in under the same landmark. At that point, are you really getting any benefit from a file with just one tiny comment and 200 lines of code? No.
Take, as an example, /model/primitives.ts
. This file’s sole purpose is to provide models for primitive values that are used repeatedly across models targeting more complex data structures, such as database rows, API inputs and outputs, and form validation.
Here’s how the file is broken up, with a few examples of what you’d find inside…
// import
// numbers
const Int
const IntPos
const IntNeg
const Float
const FloatPos
const FloatNeg
const Percent
const PercentPos
const PercentNeg
// strings
const Str
const StrNotEmpty
const StrInt
const StrFloat
const StrUrl
const StrAbsoluteUrl
const StrRelativeUrl
const StrDomain
const StrWildDomain
const StrMatch = (rx: RegExp) => {};
// dates
Does everything in this file belong together? Hell yes. Would it be better written as:
// import
// models
Hell no!
And thus, on the margin, we fall back on intuition. This doesn’t mean that the pattern is wrong or not useful. It simply means it doesn’t apply 100% of the time.
Only one question remains: When do you apply a pattern, and when do you break it?
The Rule of Patterns
Remember, our goal is to reduce the cognitive overhead of reading our code. Great code has a high signal-to-noise ratio.
Thus, there’s only one reason to apply a pattern and only one reason to break it.
Only break a pattern when the expected return is greater than that of maintaining the pattern in error.
Why is this the rule?
First, patterns derive much of their value from consistency. Whenever possible, it’s better to maintain a pattern than break it.
Second, there is a switching cost for the reader. Even if breaking the pattern would offer a marginal gain, unless that gain is greater than the burden of the break itself, it’s better to stick to the pattern. Even if you’re losing a little on the margins.
Third, if you find that it’s often better to break a pattern than maintain it, even despite the switching cost, you’ve learned something very valuable… You don’t have a good pattern at all. Instead, you have an orthodoxy. And thus, you have a fantastic opportunity to seek out a better pattern, or at minimum, criteria for when to apply the pattern you do have.
A call to arms
Patterns are a wonderful tool when applied correctly.
As we close this discussion, think back on your recent work. Are there habits you find yourself repeatedly drawn to? When do they work well? Where do they fail you?
Put your unconscious intuitions to words. Attempt to transform them into patterns. This practice is remarkably useful for improving your own abilities. Like Michael Jordan watching videos of himself playing, so that he can make corrections his unconscious mind may never have stumbled upon.
And by all means, share the patterns you discover. Seek out those who offer clarifications, questions, or improvements. There’s an infinity of optimization ahead of us, and we all contribute.