Skip to content

Instantly share code, notes, and snippets.

@nadvolod
Created January 9, 2026 15:19
Show Gist options
  • Select an option

  • Save nadvolod/71f1f830d7eafd3e946954eb9b7e8dcd to your computer and use it in GitHub Desktop.

Select an option

Save nadvolod/71f1f830d7eafd3e946954eb9b7e8dcd to your computer and use it in GitHub Desktop.
Clean code best practices

Clean Code — Patterns & Anti-Patterns (LLM Coding Assistant Guidelines)

Based on Clean Code by Robert C. Martin (summarized). Use this as a checklist when proposing or reviewing code.


✅ Core Principles (Patterns)

  • Expressive naming

    • Use intention-revealing, precise names (elapsedMs, not d).
    • Use pronounceable, searchable names; avoid encodings/Hungarian notation.
    • Prefer domain terms over abbreviations; keep consistent vocabulary.
  • 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/if chains 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.

❌ Code Smells & Anti-Patterns (Avoid)

  • 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.”

Checklists for the LLM

Function Review (before proposing code)

  • 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?

Class/Module Review

  • 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.

Test Review

  • 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.

Concurrency Review

  • Shared state minimized; immutability preferred.
  • Ownership and lifetimes explicit; timeouts/cancellation covered.
  • Idempotency and retry behavior verified; race conditions considered.

Refactoring Playbook (LLM Actions)

  • 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)

Error Handling Rules (LLM Defaults)

  • Throw/propagate specific exceptions with context (operation, inputs, id).
  • Do not return null/sentinel codes for errors; avoid null returns in general.
  • Keep try/catch narrow; handle once, near the boundary.
  • Log at the boundary; not in low-level pure functions.

Naming Rules (LLM Defaults)

  • 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).

“Clean Code” Prompts (Self-Check)

  • “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?”

Quick Anti-Pattern Map → Fix

  • Long function → Extract functions, raise abstraction.
  • Boolean/flag param → Split into two functions or use strategy.
  • Repeated switch on 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.

TypeScript Examples — Bad vs Good

1) Flag Parameters & Mixed Abstractions

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;
}

3) Tell, Don’t Ask (Encapsulation)

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();

4) Error Handling — Specific Exceptions & Narrow try/catch

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);
  }
}

5) Parameter Object & Cohesion

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
}

6) Separate Query from Modifier

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;
}

7) Replace Conditional with Polymorphism

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);

8) Immutability & Pure Helpers

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) };
}

9) Tests that Document Behavior (AAA)

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");
});

Final Rule

If the code isn’t obvious to a careful reader, refactor until it is—then comment only the why.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment