There is a moment, familiar to any developer who has spent real time in the craft, when you sit down at your keyboard to solve a problem and realize you have no idea what you're about to type. The cursor blinks. The editor is open. The task is defined — or at least assigned. And yet, your fingers hover above the keys like a pianist who has forgotten the first note of a piece they've played a thousand times. It's not that you don't know how to code. It's that you haven't yet figured out what to think.

I've been writing software for over two decades now, across enough languages and frameworks to fill a modest bookshelf. I've built APIs, orchestrated deployments, wrangled databases, and debugged production systems at three in the morning while my family slept. Through all of it, one practice has returned more value than any technology choice, any architectural pattern, any tooling upgrade: writing things down.

Not documentation. Not Jira tickets. Not Slack messages that disappear into the ether after forty-eight hours. I mean the deliberate, unhurried act of articulating your thinking in prose before you touch a line of code.

The best code I've ever written was the code I decided not to write. Every line is a liability; every abstraction is a bet against future understanding.

The Problem with Thinking in Code

We have built an entire industry around the idea that writing code is the primary activity of software development. Our tools reflect this assumption: IDEs that auto-complete our thoughts before we've finished having them, CI pipelines that validate our work in seconds, code review systems that focus on the artifact rather than the intention behind it.

But code is a notation, not a thought process. It's the final expression of a decision — often dozens of decisions — that have already been made. When we skip straight to code, we're not "moving fast." We're deferring the hardest part of the work: understanding what we're actually trying to accomplish and why.

I can't count the number of times I've watched a talented engineer spend three days building an elegant solution to the wrong problem. Not because they lacked skill, but because they never paused long enough to interrogate the problem itself. The code compiled. The tests passed. The PR was approved. And then, in a sprint retrospective two weeks later, someone quietly asked: "Wait, why did we build this?"

A Different Kind of Practice

The practice I'm describing is deceptively simple. Before writing code, I open a plain text file and write. Not a design document with UML diagrams and stakeholder sign-off sections. Just prose. I describe the problem as I understand it. I list what I know and what I don't. I write down the questions I'm afraid to ask because they might reveal how little I understand. I sketch out possible approaches, not in pseudocode but in sentences.

Here's what one of these notes looks like in practice. It's not pretty. It's not meant to be:

thinking.md
## Session Config Refresh - Jan 12, 2026

### What I think the problem is:
The session store is returning stale configuration after
a tenant updates their settings. Users report that changes
"don't stick" until they log out and back in. Support is
getting 3-4 tickets a week about this.

### What I actually know:
- Config is cached in Redis with a 15-minute TTL
- The settings UI writes directly to Postgres
- There's no cache invalidation on write
- The session middleware reads config on every request
  but only from Redis, never from Postgres directly

### What I don't know:
- Why was the TTL set to 15 minutes? Who decided this?
- Are there other consumers of this cache?
- What happens if we invalidate aggressively - will
  Redis become a bottleneck under load?
- Is the real problem the cache, or is it that we're
  caching at the wrong layer entirely?

### Questions I need to ask before writing any code:
1. What's the actual SLA for config propagation?
   (Is "immediate" really the requirement, or is
   "within 30 seconds" acceptable?)
2. Can we talk to the original author of the cache
   layer? There might be context I'm missing.
3. Is this a symptom of a larger state management
   issue we're going to hit again?

That document took fifteen minutes to write. But it saved me from a week of building the wrong thing. Because when I brought those questions to the team meeting, the tech lead mentioned something I'd never have discovered in the code: the 15-minute TTL was a workaround for a memory leak in an older version of the Redis client that had been patched six months ago. The whole caching strategy was a relic of a constraint that no longer existed.

The fix wasn't a new cache invalidation system. It was removing the cache entirely and letting the session middleware read directly from Postgres, which it had been designed to do in the first place.

An aerial view of a river delta branching into countless smaller streams, metaphor for the branching paths of software decisions
Like a river delta, software decisions branch endlessly. Writing helps you see the landscape before you commit to a path.

The Compounding Returns of Clarity

Writing before coding doesn't just prevent wrong turns. It compounds over time in ways that are difficult to quantify but impossible to ignore. Every document you write becomes a record of your thinking — a trail of breadcrumbs that future-you (or your successor, or the on-call engineer at 2 AM) can follow back to the moment a decision was made.

I have a directory on my machine called /notes/decisions that contains over eight hundred plain text files, each one a snapshot of a moment when I sat down to think through a problem. Some are two paragraphs. Some are five pages. None of them are formal. All of them have saved me at some point.

The value of these notes isn't in the writing. It's in the thinking the writing forces. The act of translating a vague intuition into a specific sentence is, itself, the work. The document is just a byproduct.

Let me show you a concrete example. Last year, I was designing a service that needed to process webhook events from multiple third-party providers. The naive approach was obvious: build a unified handler with a provider abstraction layer. But when I sat down to write through the problem, I realized something important:

JavaScript
// What I almost built:
class WebhookHandler {
  constructor(provider) {
    this.adapter = AdapterFactory.create(provider);
  }

  async process(event) {
    const normalized = this.adapter.normalize(event);
    await this.validate(normalized);
    await this.persist(normalized);
    await this.dispatch(normalized);
  }
}

// What I actually built, after writing through the problem:
// Separate handlers per provider, sharing only the
// persistence and dispatch layers. Because the "normalize"
// step was hiding the fact that each provider's event
// model was fundamentally different in ways that mattered
// for validation and error handling.

const handlers = {
  stripe:  new StripeWebhookHandler(persistence, dispatch),
  github:  new GitHubWebhookHandler(persistence, dispatch),
  sendgrid: new SendGridWebhookHandler(persistence, dispatch),
};

// Each handler knows its own shape, validation rules,
// retry semantics, and failure modes. The abstraction
// is in the shared infrastructure, not the event model.

The first approach looks cleaner on a whiteboard. The second is what actually works in production, because it respects the irreducible differences between providers instead of papering over them with a premature abstraction. I only saw this because writing forced me to articulate exactly what "normalize" meant for each provider — and it turned out, it meant something different every time.

§

Writing as a Team Practice

The benefits of writing multiply when it becomes a team practice rather than an individual habit. I've worked on teams where every significant technical decision began with a short written proposal — not an RFC with a formal template, but a brief document that answered three questions:

What problem are we solving? (Not what feature are we building, but what problem — described in terms of user impact or business value, not implementation detail.)

What have we considered? (At least two approaches, with honest tradeoffs. If you can only think of one approach, you haven't thought long enough.)

What are we choosing, and why? (The decision, stated plainly, with the reasoning that led to it.)

These documents did something magical to our meetings. Instead of spending an hour debating options that people had formed opinions about five minutes earlier, we'd spend fifteen minutes reading, then thirty minutes asking good questions about a well-articulated proposal. The quality of our decisions improved dramatically. The speed of our meetings increased. And perhaps most importantly, the junior developers on the team got to see how senior engineers actually think — not in the final, polished form of a code review, but in the messy, honest form of a written thought process.

On the Fear of the Blank Page

I should acknowledge that writing is hard. It is, in many ways, harder than coding, because code gives you the illusion of progress while writing demands that you confront the gaps in your understanding. You can write a function without knowing why. You can't write a paragraph about a function without knowing why.

This difficulty is precisely the point. The resistance you feel when staring at a blank page is the resistance of your mind encountering the boundary between what it knows and what it only thinks it knows. Pushing through that resistance is the most productive thing you can do.

You don't need to be a good writer. You don't need grammar, or structure, or transitions. You need honesty and specificity. "We need to refactor the auth layer" is a thought you haven't finished thinking. "The auth layer currently checks permissions in three different places, which means a change to the permission model requires changes in three files, and last month this caused a bug that took two days to find" — that is a thought you can act on.

You can write a function without knowing why. You can't write a paragraph about a function without knowing why. This difficulty is precisely the point.

A Practical Framework

If you'd like to try this practice, here is the minimal version that has worked for me across dozens of projects and team configurations:

Before starting any task that will take more than an hour, spend ten minutes writing about it. Open a plain text file. Write down what you think the problem is, what you know, what you don't know, and what questions you have. Don't organize it. Don't format it. Just write.

Before any meeting where a decision will be made, write a one-page summary of the options and tradeoffs. Share it at least an hour before the meeting. Ask people to read it before they arrive.

After any significant debugging session, write a brief postmortem for yourself. What was the bug? What led you to the root cause? What was the fix? What could have prevented it? These notes will save you hours the next time you encounter something similar.

Shell
# My actual workflow. Dead simple.

# Start of any significant task:
mkdir -p ~/notes/decisions/$(date +%Y)
vim ~/notes/decisions/$(date +%Y/%m-%d)-session-cache-issue.md

# I use a simple template, but even that's optional:
# ## Problem
# ## What I Know
# ## What I Don't Know
# ## Options
# ## Decision
# ## Follow-up

The Long View

We live in an era that celebrates speed. Ship fast. Break things. Move quickly and iterate. There is real wisdom in these mantras — but they are incomplete. Speed without direction is just chaos. Iteration without understanding is just repetition.

Writing is the practice of directing your speed. It is the ten minutes that saves ten hours. It is the paragraph that replaces the meeting. It is the note-to-self that becomes the decision record that becomes the institutional knowledge that prevents the next engineer from making the same mistake you almost made.

Close-up of aged, leather-bound notebooks stacked on a wooden shelf, spines worn from use
Thinking captured becomes thinking compounded.

I keep a quote taped to my monitor, written by the physicist Richard Feynman, who was himself a remarkable writer:

"The first principle is that you must not fool yourself — and you are the easiest person to fool."

Code lets you fool yourself. It gives you the syntax of progress without requiring the substance of understanding. Writing strips that away. It forces you to confront what you actually know, in the starkest and most honest terms available to us: plain language.

If you take nothing else from this essay, take this: the next time you sit down to solve a hard problem, close your editor. Open a blank file. And write. Not code. Words. Tell yourself, in complete sentences, what you're about to do and why. If you can't do it, you're not ready to code.

And if you can — you'll write better code than you ever have before.