DRY in Documentation
When to Use
When writing and maintaining technical documentation, API docs, code comments, and system documentation.
Decision Framework
| Documentation Type | Single Source | Generation Method |
|---|---|---|
| API documentation | Code (types, docstrings, annotations) | JSDoc, Sphinx, Doxygen, OpenAPI |
| Database schema | Migration files or ORM models | Schema visualization tools |
| Configuration options | Schema definitions (JSON Schema, Zod) | Auto-generated reference docs |
| Type definitions | TypeScript/types in source code | Generated .d.ts files |
| Architecture diagrams | Code structure | Automated diagram generation |
| User guides | Feature code + docstrings | MkDocs, Docusaurus from markdown |
The Problem: Documentation Drift
# ANTI-PATTERN: Documentation duplicates and drifts
def calculate_discount(price, customer_type):
"""
Calculate discount for a customer.
Args:
price: Item price
customer_type: Either 'regular' or 'premium'
Returns:
Discounted price
"""
# Code was updated but docs weren't!
if customer_type == 'vip': # Was 'premium', now 'vip'
return price * 0.8
elif customer_type == 'gold': # New tier not in docs
return price * 0.9
return price
# Documentation is now wrong and misleading
Pattern: Code as Documentation (DRY)
// GOOD: Types are documentation
type CustomerType = 'regular' | 'vip' | 'gold';
interface DiscountRule {
customerType: CustomerType;
multiplier: number;
}
const DISCOUNT_RULES: DiscountRule[] = [
{ customerType: 'vip', multiplier: 0.8 },
{ customerType: 'gold', multiplier: 0.9 },
{ customerType: 'regular', multiplier: 1.0 },
];
/**
* Calculate discount for a customer.
*
* @param price - Item price in cents
* @param customerType - Customer tier (see {@link CustomerType})
* @returns Discounted price
*
* @example
* ```ts
* calculateDiscount(1000, 'vip') // Returns 800
* ```
*/
function calculateDiscount(price: number, customerType: CustomerType): number {
const rule = DISCOUNT_RULES.find(r => r.customerType === customerType);
return price * (rule?.multiplier ?? 1.0);
}
// API docs auto-generated from JSDoc + TypeScript types
// No manual documentation to maintain
Generated Documentation
# Python example with Sphinx autodoc
class UserService:
"""
Service for managing user accounts.
This service provides CRUD operations for users and handles
authentication logic.
"""
def create_user(self, email: str, password: str) -> User:
"""
Create a new user account.
Args:
email: Valid email address for the user
password: Password (min 8 characters)
Returns:
Created user object
Raises:
ValidationError: If email is invalid or password too short
DuplicateError: If email already exists
Example:
>>> service.create_user('test@example.com', 'password123')
User(id=1, email='test@example.com')
"""
# Implementation
pass
# Sphinx generates HTML docs directly from docstrings
# Types are checked by mypy, docs always match code
OpenAPI/Swagger (Schema-Driven Docs)
# openapi.yml — Single source for API contract
openapi: 3.0.0
paths:
/users:
post:
summary: Create a new user
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
responses:
'201':
description: User created
content:
application/json:
schema:
$ref: '#/components/schemas/User'
components:
schemas:
CreateUserRequest:
type: object
required: [email, password]
properties:
email:
type: string
format: email
password:
type: string
minLength: 8
# Generate from this single source:
# - API documentation (Swagger UI)
# - Client SDKs (openapi-generator)
# - Server stubs
# - Validation schemas
# - Type definitions
Architecture Documentation
# ANTI-PATTERN: Hand-drawn diagrams in separate docs
# (Always out of date, no validation against actual architecture)
# BETTER: Generate diagrams from code structure
# Tools: Mermaid (from markdown), PlantUML, Structurizr
# Example: Generate component diagram from code structure
from pathlib import Path
import ast
def generate_dependency_graph(src_dir):
"""Auto-generate dependency diagram from imports."""
modules = {}
for py_file in Path(src_dir).rglob('*.py'):
tree = ast.parse(py_file.read_text())
imports = [node.module for node in ast.walk(tree)
if isinstance(node, ast.Import)]
modules[py_file.stem] = imports
# Generate Mermaid diagram
print("graph TD")
for module, deps in modules.items():
for dep in deps:
print(f" {module} --> {dep}")
# Output is always accurate — derived from actual code
Common Mistakes
- Writing documentation separately from code — Guaranteed to drift out of sync
- Documenting what the code does — Code should be self-documenting; explain WHY, not WHAT
- No examples in documentation — Examples are the most valuable part and easiest to auto-generate from tests
- Manual API documentation — Auto-generate from types, schemas, and docstrings
- Outdated diagrams — Generate from code structure, don't draw manually
- Copy-pasting code into docs — Link to source or include file references that auto-update