| name | coding-principles |
| description | Use when making implementation decisions, deciding how to structure code, choosing between simple and clever approaches, managing dependencies, applying 12-factor principles, handling destructive operations safely, or applying incremental delivery discipline. Use when starting to implement a feature and needing a guiding framework for day-to-day coding decisions. Triggers: "how should I write this", "should I abstract this", "is this too complex", "what dependencies should I use", "incremental delivery", "simplicity", "YAGNI". |
Coding Principles
Day-to-day engineering discipline for writing code that is simple, correct, and maintainable.
Core principle: The best code is code you don't write. The second best is code that is obvious.
When to Use
- Starting implementation of a feature
- Deciding whether to abstract or generalize
- Choosing or evaluating dependencies
- Applying 12-factor app principles
- Reviewing your own code before submitting
When NOT to Use
- Architectural decisions spanning multiple systems (use system-design)
- API/interface design (use api-design)
- Refactoring existing code (use refactoring)
Simplicity First
The Complexity Budget
Every feature starts with zero complexity credit. Each abstraction costs from that budget.
Prefer earlier in this hierarchy:
- Delete the code entirely (best)
- Inline the logic — no abstraction
- Simple function — one level of indirection
- Module with clear interface
- Framework or library (last resort)
Scope Discipline
| Request | Do it? | Rule |
|---|
| "Make it configurable" | Only with 2+ actual callers today | |
| "Add a hook for future" | Only if future use is confirmed | |
| "Generalize this" | Only with 3+ concrete cases | |
| "Error handling for X" | Only if X can actually happen | |
Three concrete cases before abstracting. One case stays inline.
What Not to Write
- Error handling for things that cannot happen
- Validation for internal function arguments
- Feature flags for changes you're making now
- Backwards-compat shims for code with no callers
- Helpers used by exactly one call site
- Comments that describe what the code does (make the code say it)
Surgical Changes
Touch only what the task requires. When editing existing code:
- Don't improve adjacent code, comments, or formatting
- Don't refactor things that aren't broken
- Match existing style, even if you'd do it differently
- If you notice unrelated dead code, flag it — don't delete it
Every changed line should trace directly to the user's request.
Incremental Delivery
Commit Discipline
Work pattern: Implement slice → Test → Verify → Commit → Next slice
- Each commit: one logical change, passing tests, ~50-200 lines
- Never "WIP" commits on shared branches
- Commit message states WHY, not WHAT (the diff shows what)
feat: add task creation endpoint with validation
Validates title length and due date before persisting.
Required by mobile team for v2.1 launch.
Vertical Slices
Build end-to-end features, not horizontal layers:
❌ Week 1: all DB models, Week 2: all services, Week 3: all endpoints
✅ Day 1: create task (full stack), Day 2: list tasks, Day 3: complete task
Each slice is working, tested, and deployable.
Dependency Management
Decision Matrix
| Factor | Use | Avoid |
|---|
| Lines replaced | 100+ lines | < 20 lines |
| Maintenance activity | Active, recent commits | Abandoned (>1yr) |
| Download count | 100k+/week | < 1k/week |
| License | MIT/Apache | GPL/AGPL (check) |
| Security history | Clean | Multiple CVEs |
Rules
- One dependency per job — not 3 libraries doing date formatting
- Pin exact versions in production (
4.17.21 not ^4.17.21)
- Audit before adding:
npm audit, check last commit, check issues
- Prefer native platform APIs when sufficient
- Small utility (<100 lines)? Copy and own it instead
Destructive Command Awareness
Before any write/delete/update, answer:
- Reversible? — Soft delete vs hard delete. Archive vs purge.
- Blast radius? — One record? All records? Cascading effects?
- Confirmation gate? — For bulk operations, require explicit confirmation.
await task.update({ deletedAt: new Date() });
if (!options.force) {
throw new Error('Pass { force: true } to permanently delete');
}
await task.destroy();
For bulk operations (>100 records), always preview before executing:
const count = await Task.count({ where: filter });
console.log(`Will delete ${count} tasks. Pass confirm: true to proceed.`);
if (!options.confirm) return;
12-Factor App
| Factor | Practice |
|---|
| Config | Environment variables only. Never config in code. |
| Processes | Stateless. Session in DB/cache, never in-process memory. |
| Dependencies | Explicit in package.json. Nothing assumed from environment. |
| Backing services | DB, cache, queue as attached resources via URL/env. |
| Build/run | Strict separation. Build once, run many environments. |
| Disposability | Fast startup (<5s), graceful shutdown (drain in-flight). |
| Dev/prod parity | Same backing services in dev and prod (Docker Compose). |
| Logs | Stdout only. App never writes log files. |
| Admin processes | Run as one-off commands in same environment as app. |
Common Rationalizations to Reject
| Rationalization | Reality |
|---|
| "I'll refactor it later" | You won't. Write it right the first time. |
| "This abstraction will pay off" | Only with 3+ concrete uses. |
| "It's just a small exception" | Exceptions compound into architecture. |
| "I'll add tests after" | Tests reveal design problems. Write them first. |
| "It's obvious, no comment needed" | Future you at 2am disagrees. |
Goal-Driven Execution
Transform vague tasks into verifiable goals before implementing:
- "Add validation" → "Write tests for invalid inputs, then make them pass"
- "Fix the bug" → "Write a failing test that reproduces it, then make it pass"
For multi-step tasks, state a brief plan:
1. [Step] → verify: [how you'll confirm it worked]
2. [Step] → verify: [how you'll confirm it worked]
Strong success criteria let you loop independently. Weak ones ("make it work") require constant clarification.
Verification Checklist