Every developer has read "Clean Code" by Robert Martin. Few teams actually enforce its principles consistently. The gap between knowing what clean code looks like and shipping it under deadline pressure is where most codebases decay.
At Privum, we have distilled years of production experience into 12 rules that we enforce across every project — from internal tools to client-facing platforms handling millions of requests. These are not aspirational guidelines; they are concrete practices backed by code reviews, linters, and CI gates.
1. Functions Do One Thing
A function should do one thing, do it well, and do it only. If you need the word "and" to describe what a function does, it should be two functions.
Bad:
validateAndSaveUser(data) — this validates AND saves. If validation fails, does it still try to save? If saving fails, does it roll back validation state? The coupling creates ambiguity.
Good:
validateUser(data) → saveUser(validatedUser) — each function has a single responsibility, a single reason to fail, and a single thing to test.
The rule of thumb: if a function is longer than 20 lines, it is probably doing more than one thing. Extract the second thing.
2. Name Things for What They Do, Not What They Are
Variable and function names should reveal intent. A reader should understand what the code does without reading the implementation.
Bad: const d = new Date() — what is "d"? A date, but which date? Why do we need it?
Good: const subscriptionExpiresAt = new Date(user.trialEnd) — now anyone reading this knows exactly what it represents and why it exists.
For boolean variables, use prefixes that read as questions: isActive, hasPermission, canDelete, shouldRetry. Code that reads like English is code that does not need comments.
3. Fail Fast, Fail Loud
Do not silently swallow errors. Do not return null when something goes wrong. Do not log an error and continue as if nothing happened.
Bad: try/catch that catches everything and returns an empty array — the caller has no idea that something failed, and debugging becomes a nightmare.
Good: Validate inputs at the boundary (API handlers, form submissions), throw typed errors when invariants are violated, and let the error propagate to a handler that knows how to respond (return 400, show toast, retry).
Silent failures are the most expensive bugs. They do not crash your app — they corrupt your data slowly over weeks until someone notices.
4. Write Tests for Behavior, Not Implementation
Tests should verify what the code does, not how it does it. If you refactor the internals and your tests break, your tests are testing the wrong thing.
Bad: Asserting that a specific private method was called with specific arguments — this test is coupled to implementation details and will break on any refactor.
Good: Given an input, assert the output or side effect. "When a user submits a valid form, they should see a success message and the data should be persisted." This test survives refactoring because it tests behavior.
Coverage metrics are useful but misleading. 80% coverage with good behavioral tests is better than 100% coverage with brittle implementation tests.
5. Keep Dependencies at the Edges
Business logic should not import HTTP clients, database drivers, or framework-specific code. Push I/O to the edges and keep the core pure.
This is not academic advice — it has practical consequences: - Pure business logic is trivially testable (no mocks needed) - Switching from PostgreSQL to MongoDB changes the adapter, not the domain - Framework upgrades do not cascade through your entire codebase
The pattern: Controllers/Handlers → Use Cases/Services → Domain Logic. Dependencies flow inward. The domain never imports from infrastructure.
6. Prefer Composition Over Inheritance
Inheritance creates tight coupling. When you inherit from a base class, you inherit its entire contract — including parts you do not need and bugs you did not write.
Composition gives you flexibility: combine small, focused pieces instead of building deep hierarchies. In TypeScript/JavaScript, this means preferring plain functions and objects over class hierarchies.
If you find yourself creating a class hierarchy deeper than two levels, stop and refactor. The complexity is not worth the abstraction.
7. Handle Errors at the Right Level
Not every function should handle every error. Errors should be handled at the level that has enough context to make the right decision.
A database query function should not decide what HTTP status code to return — it does not know it is being called from an HTTP handler. It should throw a typed error that the handler can interpret.
Layer your error handling: - Domain layer: throw domain-specific errors (UserNotFound, InsufficientBalance) - Application layer: catch domain errors, decide on response strategy - Infrastructure layer: catch transport errors, implement retries and circuit breakers
8. Make Illegal States Unrepresentable
Use your type system to prevent bugs at compile time. If a user can be either "active" or "suspended", do not use a boolean — use a union type or enum. If an order must have at least one item, do not use an array — use a type that guarantees non-emptiness.
TypeScript example: instead of { status: string }, use { status: 'active' | 'suspended' | 'deleted' }. Now the compiler catches typos and invalid states before runtime.
The more invariants you encode in types, the fewer runtime checks you need and the fewer bugs reach production.
9. Code Reviews Are Not Optional
Every line of code that reaches production should be reviewed by at least one other engineer. No exceptions — not for "quick fixes", not for "just config changes", not for the tech lead.
Code reviews catch bugs, share knowledge, enforce standards, and create shared ownership. They are the single most effective quality practice after automated testing.
Keep reviews small (under 400 lines). Review within 24 hours. Comment on patterns, not preferences. Ask questions instead of making demands.
10. Delete Dead Code
Dead code is not free. It confuses new team members, increases cognitive load, and occasionally gets accidentally re-enabled. If code is not called, delete it. Git remembers.
This includes: - Commented-out code blocks - Unused imports and variables - Feature flags that have been permanently on/off for months - API endpoints that nothing calls
Run a dead code analysis tool quarterly. You will be surprised how much accumulates.
11. Log with Purpose
Logging everything is as useless as logging nothing. Good logs answer: "what happened, when, to whom, and what was the context?"
Structure your logs as JSON with consistent fields: timestamp, level, service, requestId, userId, action, duration, error. This makes them searchable and aggregatable.
Use log levels correctly: - ERROR: something failed and needs attention - WARN: something unexpected happened but was handled - INFO: significant business events (user signed up, payment processed) - DEBUG: technical details useful during development (query timing, cache hits)
In production, run at INFO level. Enable DEBUG only when investigating specific issues.
12. Automate Your Standards
Rules that require human discipline to enforce will be violated under pressure. Encode your standards in tooling:
- Linting: ESLint, Prettier, or Biome for formatting and style
- Type checking: TypeScript strict mode, no
any - Pre-commit hooks: Husky + lint-staged to catch issues before they reach CI
- CI gates: Tests, type check, and lint must pass before merge
- Dependency scanning: Renovate or Dependabot for automated updates
If a rule is important enough to enforce, it is important enough to automate. Human code reviewers should focus on logic, architecture, and clarity — not indentation and import order.
Conclusion
Clean code is not a luxury — it is an economic decision. Every hour invested in code quality saves ten hours in debugging, onboarding, and maintenance. Teams that enforce these practices ship faster (not slower), because they spend less time fighting their own codebase and more time building features.
The best time to adopt these practices is at the start of a project. The second best time is now. Pick three rules from this list, add them to your team's definition of done, and enforce them in your next sprint. Improvement is incremental — but it compounds.