Six Rules That Help You Write Clean Code
Clean code is code you can verify by reading it. The next person opens a file, follows what it does, and changes it without fear. That next person is usually you, six months from now.
Most advice about clean code comes back to six rules. You've probably heard all of them. The problem is that each one is easy to apply too hard, and an over-applied rule makes code worse. So for each rule, here's what it means, how I apply it, and where I stop.
Separation of Concerns (SOC)
Break a program into smaller units, each focused on one task.
The split I care about most is between decisions and actions. A decision is a pure function: it takes data and returns an answer. An action does I/O: it fetches, writes to the DOM, sets state, shows a toast. When the two are mixed, you can't test the decision without faking the world around it.
Split it by responsibility, and keep the pieces in the same file:
calculateTotal now runs in a test with no network and no browser. The API response is parsed at the edge, so a bad payload fails loudly at the line that received it instead of somewhere downstream.
Where I stop: separating concerns is not the same as separating files. I keep a budget: to understand a function, you should open at most two other files. Shared infrastructure everyone knows (the HTTP client, the UI primitives) doesn't count. If tracing a value takes five file jumps, the structure is wrong, no matter how "separated" it looks. One long, cohesive file reads better than five short ones.
Document Your Code (DYC)
Write code for your future self and for others.
Most documentation should live in the code itself:
- Names say the business meaning.
isEligibleForRenewal, notcheckFlag2. A good name removes the need for a comment. - Comments say why, never what. The code already says what. A comment earns its place when it explains a constraint you can't see.
- Files read top to bottom. Private helpers go above the export that uses them, so nothing is read before it's defined.
There's one kind of comment I replace with code every time: the assumption. A comment can't fail. A check can.
Where I stop: comments that drift. A wrong comment is worse than no comment, so they change with the code or they get deleted.
Don't Repeat Yourself (DRY)
Don't write the same thing twice. Use functions, modules, and existing libraries.
DRY is right about rules. If "free shipping starts at $50" lives in three places, a price change needs three edits and one will be missed. A business rule gets one home and a name:
DRY is wrong about code that looks alike. Two blocks with the same shape but different reasons to exist will drift apart. Merge them early, and the shared function grows a flag for the second caller, then an options object for the third.
My rule: if a function needs a flag, an options bag, or a type parameter to serve two callers, it's two functions. Two similar blocks are fine. I only unify when a third identical case shows up. I wrote more about this in Duplication Is Far Cheaper Than the Wrong Abstraction.
Where I stop: existing libraries are still the best kind of DRY. Date parsing, validation, and number formatting are solved problems. Use the solution.
Keep It Simple, Stupid (KISS)
Simple is hard, but better. Readable code beats clever code.
My test is short: if a reviewer has to think to see that it works, rewrite it.
A few habits keep code simple:
- Guard clauses first. Validate and return early, so the happy path stays unindented. No
elseafter areturn, no nested ternaries. - Call the function you mean. One level of indirection, no more. No factories that return functions, no wrappers that wrap behavior, no barrel files that re-export. "Where is this defined?" should be a one-jump question.
- Every loop and retry has a bound. No
while (true). A retry has a max attempt count, every request has a timeout. - A function fits on one screen. Around 60 lines at most. Longer than that, split it by responsibility into private pieces in the same file.
Where I stop: simple doesn't mean skipping structure the problem needs. It means no structure the problem doesn't need.
Test Driven Development (TDD)
TDD is a short loop:
- Red: write a failing test first.
- Green: write just enough code to make it pass.
- Refactor: clean up the code without changing its behaviour.
Writing the test first forces you to decide what the code does before deciding how. And the refactor step, where the actual cleaning happens, is only safe because the tests exist.
This is where separation of concerns pays off. I test decisions: parsers, calculators, mappers, query builders. They're pure, so the tests need no mocks. When a test needs a pile of mocks, the decision and the action are usually still tangled together.
Where I stop: I don't unit-test thin wiring, like a component that passes props to a button. Test what the code decides, not how it's assembled, so you can change the assembly without rewriting every test.
You Ain't Gonna Need It (YAGNI)
Build what you need now. Don't build features you think you might need later.
Every "just in case" parameter is code you read, test, and maintain for a requirement that may never arrive:
If the app goes multi-currency next year, you add the parameter then, with a real use case telling you what it should look like.
YAGNI also applies to where code lives. Declare everything at the smallest scope that works: a value in the block that uses it, a helper in the file that uses it, state in the lowest component that owns it, a file in the feature that uses it. Promote it one level only when a second consumer actually appears, not when you imagine one.
Where I stop: decisions that are expensive to reverse. You don't need a plugin system on day one, but a database schema and a public API deserve some thought up front.
How the Rules Fit Together
The rules lean on each other:
- SOC makes TDD cheap. Pure decisions are tested without mocks.
- TDD makes refactoring safe. With tests in place, you can simplify without breaking things.
- YAGNI keeps DRY honest. An abstraction built for callers that don't exist yet is the wrong abstraction waiting to happen.
Write code you can verify by reading it. If following one of these rules makes the code harder to read, the rule is being applied wrong.