DEV Community

Cover image for The 3-line discipline
Hideki Mori
Hideki Mori

Posted on

The 3-line discipline

When I write code in unfamiliar territory, I write three lines, then I run it.

Then I write three more lines, and I run it again.

I've been doing this for twenty-four years. It's the most specific habit I have. I almost didn't write this article, because the habit feels too small to be worth describing — but then I noticed that it's the part of my way of working that I can never seem to explain to someone in real time. It needs writing down.


Three principles

The discipline rests on three things I believe about writing code. They're not deep. They've just stayed with me.

1. Trust nothing but your own code.

If you can't trust the code you wrote yourself, what can you trust? Not a library, not a vendor's documentation, not your own assumption from yesterday. The only thing in the system whose behavior you can fully verify is the code you just typed, by running it.

2. Write in code, not in language.

If you're describing what the code should do in Japanese or English, you're spending the same time you could have spent writing the code itself. By the time the code runs, the description is already done — by the code, in a more precise form than any language could give it.

3. Make three lines complete.

The three lines you just wrote should be complete. Error handling included. Validation included. Logging included. Not "I'll add validation later." Not "I'll wrap it in a try-catch later." Three lines, complete, then run.

(There's a small exception to this. Sometimes you do want to ignore every error and move on — for instance, when you're trying to understand whether the happy path works at all before you care about anything else. That's a different mode, used deliberately. It's not the same as "I'll handle errors later.")


Why three lines

Three lines is roughly the unit of thought I can hold completely. Five lines, and I start guessing what the third line did. Ten lines, and I'm reading the code as if it were someone else's. Three lines is the size that stays mine.

When three lines run and produce what I expected, I keep them. When they don't, I either fix them or delete them. The cost of deleting three lines is small enough that I have no attachment to keeping them.

What I'm protecting, by writing this small, is the alignment between the code in my head and the code that's actually running. When that alignment is intact, debugging is fast: I know which three lines just changed. When that alignment slips — because I wrote thirty lines without running them — debugging becomes archaeology. I'd rather spend the time in three-line increments and avoid the archaeology.


Components: write the caller, run, write again, run

When I introduce a new component into a system — a new library, a new vendor's API, a new framework — I don't start by writing the code that needs the component. I start by writing the code that calls the component.

The caller is small at first. Three lines. I call the component, I print what comes back, I run it. Then three more lines. I call it with different arguments. I print what comes back. I run it. Three more lines. I call it in a way that should fail. I see what failure looks like.

By the time I've spent an hour doing this, I know how the component behaves on the inputs I care about, how it behaves on edge cases, how it fails, what it returns when it fails. I have a small body of code that has tested the component from the outside, written in my own hand.

After that, the component almost never surprises me when I integrate it into the real work. It surprised me already, during the hour I was poking at it, and I noted what I learned.

This part of the discipline is what twenty-four years has changed about me the most. When I started, I would try to use a new library inside the real code right away, and then I'd be debugging both the library's behavior and my own use of it at the same time. I don't do that anymore. The library has to pass a small private interview first.


Even then, live data still surprises you

Here's the part that keeps the discipline honest: even when you've done all of the above, live data will still surprise you. The vendor's API will return something the documentation never mentioned. The status code will say success while the payload says something else. The same call you've made a thousand times will, on the thousand-and-first try, return data that belongs to a different question entirely.

I worked with a fairly mainstream translation API for many years. It's the kind of API most people in the industry have heard of. In day-to-day operation, the integration was stable. But in the operational record of how my code calls it, there are several places where I had to write defenses that aren't suggested by the documentation:

  • The languages endpoint, when asked for target languages, sometimes returned the response shaped like a source language listing. The HTTP status was 200. The JSON parsed. But a field that should be present on target languages was missing. The fix wasn't to escalate or to file a bug — it was to detect the mismatch in my code, log it as a retry-worthy condition, and call the endpoint again. The next call usually returned correctly.

  • Certain error messages from the API turned out to be retry-worthy, even though the HTTP status code didn't say so. A "Temporary Error" in the response body, or a "Tag handling parsing failed", both warranted retrying. I learned this not from the documentation but from watching the production logs over many months.

None of this is a complaint about the vendor. It's a mainstream API. The point isn't that it's flawed; the point is that any API run at scale, against real data, will produce these moments. The documentation is a description of intended behavior, not observed behavior. Observed behavior, in production, is always wider.

So even after the three-line discipline, even after the private interview with the component, the system goes into production and surprises me. Not catastrophically. Quietly. A condition I hadn't tested, behaving in a way I hadn't predicted.

I don't experience this as a failure of the discipline. I experience it as the part of the work that the discipline doesn't cover — and was never going to cover. The discipline brings me to the doorstep with my code in good shape. Live data is what's on the other side of the door, and it's not something I get to fully prepare for. It's something I respond to.

This is, I think, the part of the work I find most enjoyable.


Twenty-four years of this

I didn't set out to develop a discipline. I started writing software in 2002, at a company that was almost out of money, on a product that needed to ship in thirty days. There was no time to write thirty lines and then debug them. I wrote three lines, I ran them, I wrote three more. The shape of how I work formed itself in that situation, and it never went away.

What's changed over twenty-four years is what I do with the result. The three-line increments are the same. The "write the caller first, run, run again" is the same. The willingness to be surprised by live data is the same. What's deeper now is just the cumulative trust in my own code, and the cumulative humility about everything outside it.

If you write the code you can trust, you can carry the weight of everything you can't.

This is not what you should do. This is what twenty-four years has taught one specific person to do.


Built with Claude (Opus).


Earlier in this series:

Top comments (0)