Based on Clean Code by Robert C. Martin (summarized). Use this as a checklist when proposing or reviewing code.
-
Expressive naming
- Use intention-revealing, precise names (
elapsedMs, notd). - Use pronounceable, searchable names; avoid encodings/Hungarian notation.
- Prefer domain terms over abbreviations; keep consistent vocabulary.
- Use intention-revealing, precise names (
-
Small, focused functions
- Functions do one thing; extract until it’s obviously one thing.
- Keep functions short (a few lines to ~10–20 lines); reduce branching.
- Use descriptive function names; prefer verbs for actions.
-
Clear, linear flow
- One level of abstraction per function; avoid mixed abstraction.
- Prefer polymorphism/strategy over
switch/ifchains across types. - Keep happy path prominent; handle errors separately.
-
Meaningful comments (as last resort)
- Explain why, not what; record rationale, assumptions, warnings.
- Use TODOs sparingly with owner + context.
- Prefer self-documenting code over explanatory comments.
-
Consistent formatting
- Enforce a standard style (linters/formatters).
- Keep file/class/function ordering consistent (public → protected → private).
- Vertical density communicates relatedness; group by high cohesion.
-
Objects and data structures
- Hide implementation; expose behavior via methods, not fields.
- Tell, don’t ask: push logic into the object that owns the data.
- Prefer immutability for value objects; minimize shared mutable state.
-
Error handling
- Use exceptions, not error codes; create precise exception types.
- Fail fast; validate at boundaries; keep try/catch blocks small.
- Provide context in error messages; don’t swallow exceptions.
-
Boundaries & APIs
- Isolate third-party libraries behind your interfaces.
- Write adapter/wrapper to keep the rest of the code stable.
- Cover boundary behavior with focused tests.
-
Unit tests (Clean Tests)
- Tests are first-class code: readable, fast, deterministic, isolated.
- Use AAA (Arrange-Act-Assert); 1 assertion concept per test.
- Tests document behavior; keep them free of logic/duplication.
-
DRY and duplication management
- Extract common behavior; prefer composition over inheritance.
- Keep single source of truth for constants and domain rules.
-
Cohesion & coupling
- High cohesion within modules; low coupling between modules.
- Classes small and focused on a single responsibility.
-
Concurrency hygiene
- Separate concurrency concerns from business logic.
- Minimize shared state; prefer immutability/message passing.
- Use clear ownership, timeouts, and robust cancellation.
-
System design (emergent design)
- Simple design rules: passes tests, reveals intent, no duplication, minimal classes/methods.
- Incrementally refactor as understanding improves.
- Long things — long functions, long parameter lists, long classes/files.
- Mixed abstraction — high-level policy mixed with low-level detail in the same scope.
- Flag arguments / boolean parameters — they create multiple functions in one.
- Switch/if-else type codes — repeated branching on type/state → use polymorphism/strategy.
- Inappropriate intimacy — excessive knowledge between modules; violates encapsulation.
- Temporal coupling — call order dependencies not expressed by the API.
- Hidden side effects — query-like functions that mutate state (or vice versa).
- Magic numbers/strings — unnamed constants; use well-named constants/enums.
- Comment as deodorant — comments masking bad code; refactor instead.
- Dead code — unused variables/methods/feature flags; delete ruthlessly.
- Duplicated code — copy-paste logic; extract and reuse.
- Primitive obsession — raw primitives for domain concepts; create value objects.
- Excessive getters/setters — anemic domain model; push behavior into objects.
- Exception misuse — error codes, swallowed exceptions, generic catch-alls.
- Inconsistent naming & style — mixed conventions, misleading names/abbreviations.
- Over-mocking / brittle tests — tests tied to implementation details.
- Global state / singletons — hidden couplings; prefer DI and clear scopes.
- Concurrency hazards — races, non-idempotent retries, shared mutable state.
- Speculative generality — YAGNI violations: abstractions “just in case.”
- Does the function do exactly one thing at one abstraction level?
- Is the name intention-revealing and verb-based?
- Are parameters ≤ 3 and conceptually cohesive? (Else, make a parameter object.)
- Are side effects explicit (in name/docs) or eliminated?
- Are errors handled with specific exceptions and clear messages?
- Single clear responsibility; high cohesion of methods/fields.
- No public fields; minimal surface area; invariants enforced.
- No duplication across classes; composition > inheritance when feasible.
- External libs isolated behind interfaces/adapters.
- Tests read like documentation (AAA), 1 behavior per test.
- No logic in tests (loops/conditionals minimized).
- Deterministic, fast, and independent (no order dependence).
- Failing tests provide clear, actionable messages.
- Shared state minimized; immutability preferred.
- Ownership and lifetimes explicit; timeouts/cancellation covered.
- Idempotency and retry behavior verified; race conditions considered.
- Extract Function / Variable / Class
- Replace Conditional with Polymorphism
- Introduce Parameter Object (when ≥3 related params)
- Encapsulate Collection (no external mutation)
- Replace Magic Number with Named Constant
- Inline Temp (when name adds no value)
- Remove Dead Code
- Decompose Conditional (guard clauses; early returns)
- Separate Query from Modifier
- Introduce Assertion (enforce invariants)
- Wrap Third-Party API (adapter boundary)
- Throw/propagate specific exceptions with context (operation, inputs, id).
- Do not return
null/sentinel codes for errors; avoidnullreturns in general. - Keep
try/catchnarrow; handle once, near the boundary. - Log at the boundary; not in low-level pure functions.
- Variables: nouns; functions: verbs; classes: nouns; booleans: predicates (
isEmpty). - Avoid noise words (
Data,Info,Manager) unless they differentiate meaning. - Consistent tense and domain terminology; no abbreviations unless ubiquitous (
id,url).
- “What’s the one thing this function does?”
- “What domain concept can replace these primitives?”
- “Where is the right home for this behavior (Tell, don’t ask)?”
- “Can I delete this without changing behavior?” (duplication/dead code)
- “What coupling/cohesion tradeoff am I making—and is it worth it?”
- “Does this design survive change in the top 3 likely scenarios?”
- Long function → Extract functions, raise abstraction.
- Boolean/flag param → Split into two functions or use strategy.
- Repeated
switchon type → Polymorphism or lookup table. - Many related params → Parameter object / value object.
- Getter/Setter everywhere → Encapsulate behavior in the object.
- Error codes/nulls → Exceptions / Option/Result types.
- Magic constants → Named constants/enums.
- Copy-paste tests → Helper builders that keep tests readable.
Bad
function processInvoice(d: string, save: boolean): number | null {
const data = JSON.parse(d);
if (!data.id || !data.total) return null;
if (save) {
localStorage.setItem(`inv_${data.id}`, JSON.stringify(data));
}
return data.total;
}Good
type Invoice = Readonly<{ id: string; total: number }>;
class InvoiceParseError extends Error {
constructor(public readonly raw: string, message = "Invalid invoice JSON") { super(message); }
}
export function parseInvoice(json: string): Invoice {
let obj: unknown;
try {
obj = JSON.parse(json);
} catch {
throw new InvoiceParseError(json, "Malformed JSON");
}
const { id, total } = obj as Record<string, unknown>;
if (typeof id !== "string" || typeof total !== "number") {
throw new InvoiceParseError(json, "Missing or invalid fields");
}
return { id, total };
}
export interface InvoiceStore { save(invoice: Invoice): void }
export function saveInvoice(store: InvoiceStore, invoice: Invoice): void {
store.save(invoice);
}2) Magic Numbers & Hidden Side Effects
Bad
let lastPing = Date.now();
export function isExpired(ts: number): boolean {
lastPing = Date.now();
return Date.now() - ts > 86400000;
}Good
export const MS_PER_DAY = 24 * 60 * 60 * 1000 as const;
export function isExpired(sinceEpochMs: number, now = Date.now()): boolean {
return now - sinceEpochMs > MS_PER_DAY;
}Bad
type User = { first: string; last: string; status: "active" | "suspended" };
function canLogin(u: User): boolean {
return u.status === "active" && u.first.length > 0 && u.last.length > 0;
}Good
class User {
constructor(
private readonly first: string,
private readonly last: string,
private status: "active" | "suspended"
) {}
activate() { this.status = "active"; }
suspend() { this.status = "suspended"; }
canLogin(): boolean {
return this.status === "active" && this.first !== "" && this.last !== "";
}
}
const allowed = user.canLogin();Bad
function readConfig(path: string): any | null {
try {
const text = (globalThis as any).__fsRead(path);
return JSON.parse(text);
} catch (e) {
console.error("config failed", e);
return null;
}
}Good
class ConfigReadError extends Error {
constructor(public readonly path: string, cause?: unknown) {
super(`Failed to read config at ${path}`);
this.cause = cause as any;
}
}
class ConfigParseError extends Error {
constructor(public readonly path: string, cause?: unknown) {
super(`Invalid JSON in config at ${path}`);
this.cause = cause as any;
}
}
function readText(path: string): string {
try {
return (globalThis as any).__fsRead(path);
} catch (e) {
throw new ConfigReadError(path, e);
}
}
export function loadConfig(path: string): Record<string, unknown> {
const text = readText(path);
try {
return JSON.parse(text);
} catch (e) {
throw new ConfigParseError(path, e);
}
}Bad
function scheduleEmail(to: string, subject: string, body: string, delayMs: number, retries: number) {
// ...
}Good
type EmailJob = Readonly<{
to: string;
subject: string;
body: string;
delayMs: number;
retries: number;
}>;
function scheduleEmail(job: EmailJob) {
// cohesive, easier to extend/test
}Bad
function getNextIdAndIncrement(counter: { value: number }): number {
return ++counter.value;
}Good
function peekNextId(counter: { value: number }): number {
return counter.value + 1;
}
function advance(counter: { value: number }): void {
counter.value += 1;
}Bad
type Shape = { kind: "circle"; r: number } | { kind: "rect"; w: number; h: number };
function area(s: Shape): number {
if (s.kind === "circle") return Math.PI * s.r * s.r;
if (s.kind === "rect") return s.w * s.h;
throw new Error("unknown shape");
}Good (OO)
interface Shape { area(): number; }
class Circle implements Shape {
constructor(private readonly r: number) {}
area(): number { return Math.PI * this.r * this.r; }
}
class Rect implements Shape {
constructor(private readonly w: number, private readonly h: number) {}
area(): number { return this.w * this.h; }
}Good (FP with lookup)
type Circle = { kind: "circle"; r: number };
type Rect = { kind: "rect"; w: number; h: number };
type Shape = Circle | Rect;
const areaFns: Record<Shape["kind"], (s: any) => number> = {
circle: (s: Circle) => Math.PI * s.r * s.r,
rect: (s: Rect) => s.w * s.h,
};
const area = (s: Shape) => areaFns[s.kind](s as any);Bad
function applyDiscount(cart: { total: number }, pct: number) {
cart.total = cart.total - cart.total * pct;
}Good
type Cart = Readonly<{ total: number }>;
function withDiscount(cart: Cart, pct: number): Cart {
return { total: cart.total * (1 - pct) };
}Bad
test("formats price", () => {
const p = Math.round((1000 / 3) * 100) / 100;
expect(formatPrice(333.33)).toBe(`$${p}`);
});Good
test("formats price to two decimals with currency", () => {
const input = 333.33;
const out = formatPrice(input);
expect(out).toBe("$333.33");
});If the code isn’t obvious to a careful reader, refactor until it is—then comment only the why.