DEV Community

Hive80-lab
Hive80-lab

Posted on

The Lazy Developer's Guide to Documentation That Actually Gets Read

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:

  1. What does this do?
  2. How do I run it?
  3. How do I test it?
  4. Where do I get help?
# Payment API

Processes credit card payments via Stripe. Handles webhooks, refunds, and disputes.

## Quick Start
Enter fullscreen mode Exit fullscreen mode


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
Enter fullscreen mode Exit fullscreen mode


bash
npm test # Unit tests
npm run test:e2e # End-to-end tests


## Help
- Slack: #payments-team
- Runbook: [link]
- On-call: See PagerDuty schedule
Enter fullscreen mode Exit fullscreen mode


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`
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The Rules

  1. README answers 4 questions — what, run, test, help. Nothing else.
  2. Runbooks are checklists — every step is a command, not a paragraph.
  3. Deep dives are linked — not front and center.
  4. Docs are tested — stale docs are worse than no docs.
  5. 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...
Enter fullscreen mode Exit fullscreen mode

✅ 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`
Enter fullscreen mode Exit fullscreen mode

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)