The Lazy Developer's Guide to Documentation That Actually Gets Read
Nobody reads your 47-page wiki. Here's how to write docs that people actually use.
I've written documentation for 8 years. I've also watched nobody read it. Here's what I learned: the best documentation isn't comprehensive — it's findable, scannable, and actionable.
Why Documentation Fails
Reason 1: It's Written for the Writer, Not the Reader
Most docs start with "Overview" and "Architecture." The reader wants to know: "How do I make this work?"
Reason 2: It's Too Long
A 47-page wiki means the reader has to search for the 3 paragraphs they need. They'll ask in Slack instead.
Reason 3: It's Outdated
The doc says npm start. The code uses npm run dev. The reader follows the doc, gets an error, and never trusts the docs again.
The Solution: 3-Layer Documentation
Layer 1: The README (30 seconds)
Your README should answer 4 questions in 30 seconds:
- What does this do?
- How do I run it?
- How do I test it?
- Where do I get help?
# Payment API
Processes credit card payments via Stripe. Handles webhooks, refunds, and disputes.
## Quick Start
bash
cp .env.example .env # Add your STRIPE_KEY
docker compose up # Starts API + DB + Redis
curl localhost:3000/health # Verify it's running
## Test
bash
npm test # Unit tests
npm run test:e2e # End-to-end tests
## Help
- Slack: #payments-team
- Runbook: [link]
- On-call: See PagerDuty schedule
markdown
That's it. No architecture diagram. No history. No philosophy. Just: run, test, get help.
Layer 2: The Runbook (2 minutes)
The runbook is for when things break. It should be a checklist, not a novel.
# Payment API Runbook
## Service won't start
1. Check Redis: `redis-cli ping` → should return PONG
2. Check DB: `psql -U postgres -c 'SELECT 1'` → should return 1
3. Check env: `cat .env | grep STRIPE` → should show STRIPE_KEY=sk_...
4. Restart: `docker compose restart api`
5. If still failing: check logs `docker compose logs api --tail 50`
## Payments failing
1. Check Stripe status: https://status.stripe.com
2. Check webhook: `curl -X POST localhost:3000/webhook -H "..." `
3. Check DB: `SELECT count(*) FROM payments WHERE status='failed' AND created_at > now() - interval '1 hour'`
4. If Stripe is down: enable circuit breaker `TOGGLE_CIRCUIT_BREAKER=true`
## High latency
1. Check DB connections: `SELECT count(*) FROM pg_stat_activity`
2. Check Redis memory: `redis-cli info memory | grep used_memory_human`
3. Scale up: `kubectl scale deployment payment-api --replicas=5`
Every step is a command. No prose. No explanation. Just: run this, check that.
Layer 3: The Deep Dive (only if needed)
This is your architecture doc, your data flow diagram, your design decisions. But it's linked from the README, not front and center.
## Architecture
For the full architecture document, see [ARCHITECTURE.md](./ARCHITECTURE.md).
Key decisions:
- We use Stripe for payments (not building our own PCI compliance)
- We use Redis for idempotency keys (prevent double charges)
- We use webhooks for async processing (not polling)
The Documentation Generator
#!/usr/bin/env python3
"""doc_gen.py - Auto-generate docs from code comments"""
import ast
import os
import re
from pathlib import Path
class DocGenerator:
def __init__(self, project_root):
self.root = Path(project_root)
def generate_api_docs(self):
"""Generate API docs from route definitions"""
docs = []
for py_file in self.root.rglob('*.py'):
with open(py_file) as f:
tree = ast.parse(f.read())
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
# Look for route decorators
for decorator in node.decorator_list:
if self._is_route(decorator):
method, path = self._extract_route(decorator)
docstring = ast.get_docstring(node) or ''
docs.append({
'method': method,
'path': path,
'function': node.name,
'description': docstring.split('\n')[0],
'file': str(py_file.relative_to(self.root))
})
return self._format_docs(docs)
def _is_route(self, decorator):
route_names = ['get', 'post', 'put', 'delete', 'patch', 'route']
if isinstance(decorator, ast.Call):
if isinstance(decorator.func, ast.Attribute):
return decorator.func.attr in route_names
return False
def _extract_route(self, decorator):
if isinstance(decorator, ast.Call):
if decorator.func.attr == 'route':
method = 'GET' # default
if decorator.keywords:
for kw in decorator.keywords:
if kw.arg == 'methods':
method = kw.value.elts[0].value
else:
method = decorator.func.attr.upper()
if decorator.args:
path = decorator.args[0].value
else:
path = '/'
return method, path
return 'GET', '/'
def _format_docs(self, docs):
output = '# API Documentation\n\n'
for d in sorted(docs, key=lambda x: x['path']):
output += f"## {d['method']} {d['path']}\n"
output += f"{d['description']}\n\n"
output += f"Function: `{d['function']}` in `{d['file']}`\n\n"
return output
The Freshness Checker
#!/usr/bin/env python3
"""doc_check.py - Check if docs are stale"""
import os
from datetime import datetime, timedelta
def check_doc_freshness(doc_path, code_path):
doc_time = os.path.getmtime(doc_path)
code_time = os.path.getmtime(code_path)
if code_time > doc_time:
age = datetime.now() - datetime.fromtimestamp(doc_time)
code_age = datetime.now() - datetime.fromtimestamp(code_time)
return {
'stale': True,
'doc_age_days': age.days,
'code_age_days': code_age.days,
'warning': f'Doc is {age.days} days old, code was modified {code_age.days} days ago'
}
return {'stale': False}
def audit_all_docs(docs_dir, code_dir):
stale = []
for doc_file in Path(docs_dir).rglob('*.md'):
corresponding_code = find_code_for_doc(doc_file, code_dir)
if corresponding_code:
result = check_doc_freshness(doc_file, corresponding_code)
if result['stale']:
stale.append({
'doc': str(doc_file),
'warning': result['warning']
})
return stale
The Rules
- README answers 4 questions — what, run, test, help. Nothing else.
- Runbooks are checklists — every step is a command, not a paragraph.
- Deep dives are linked — not front and center.
- Docs are tested — stale docs are worse than no docs.
- Code comments explain WHY — not WHAT. The code shows what.
The Anti-Patterns
❌ Don't: The Novel
# Payment Service
## Overview
The payment service is a critical component of our infrastructure...
## History
We started building this in 2023 when we realized...
## Architecture
Our system follows a microservices pattern...
✅ Do: The Cheat Sheet
# Payment Service
## Run
`docker compose up`
## Test
`npm test`
## Common Issues
- Redis down → restart: `docker compose restart redis`
- Stripe 500 → check status.stripe.com
- DB timeout → check connections: `SELECT count(*) FROM pg_stat_activity`
Want the complete documentation toolkit? The Ops Starter Kit includes documentation templates, freshness checkers, and auto-generators — everything you need to write docs that actually get read.
When was the last time you read your own documentation?
Top comments (0)