Effective Technical Documentation for Engineering Teams
How to write technical documentation that engineers actually read — covering ADRs, runbooks, API docs, and documentation-as-code workflows.

Documentation that nobody reads is not documentation — it is wasted effort. Most engineering teams have the wrong documentation: outdated wiki pages, sprawling READMEs, and Confluence graveyards. The problem is not that engineers do not like writing. The problem is that most documentation rots because it is disconnected from the code it describes.
Effective documentation lives close to the code, is written for a specific audience, and stays current through automated checks.
Architecture Decision Records (ADRs)
ADRs capture the why behind technical decisions. Six months from now, nobody will remember why you chose PostgreSQL over DynamoDB. The ADR does.
## ADR-007: Use PostgreSQL for Order Service
### Status
Accepted (2021-08-15)
### Context
The order service needs a primary data store. Requirements:
- Strong consistency for financial transactions
- Complex queries for reporting (joins, aggregations)
- Up to 10M orders/year with 5-year retention
- Team has existing PostgreSQL expertise
### Options Considered
**PostgreSQL**
- Pros: ACID transactions, strong query language, team expertise
- Cons: Horizontal scaling requires manual sharding
**DynamoDB**
- Pros: Managed scaling, predictable latency at any scale
- Cons: Limited query patterns, eventual consistency by default,
team would need training
**MongoDB**
- Pros: Flexible schema, good for document-shaped data
- Cons: Weaker transaction support, our data is relational
### Decision
PostgreSQL with read replicas for reporting queries.
### Rationale
Our query patterns are relational (orders → items → customers).
10M orders/year fits comfortably on a single PostgreSQL instance
with proper indexing. The team has 4 years of PostgreSQL experience
vs. zero DynamoDB experience. We can revisit if we exceed 100M
orders/year, which is 3+ years out on current growth.
### Consequences
- Need to manage connection pooling (pgBouncer)
- Reporting queries routed to read replica to protect write latency
- Schema migrations managed via FlywayThe ADR answers the question future engineers will ask: "Why did we pick this?" Store ADRs in the repository (docs/adr/) so they travel with the code.
Runbooks
Runbooks are step-by-step guides for operational tasks: deployments, incident response, and common debugging scenarios. They should be executable by someone who has never seen the system before.
## Runbook: Database Connection Pool Exhaustion
### Symptoms
- API response times spike above 5 seconds
- Error logs show: `Error: connection pool exhausted`
- Grafana dashboard: `pgbouncer_active_connections` at max
### Diagnosis
1. Check current connection count:
```sql
SELECT count(*) FROM pg_stat_activity
WHERE state = 'active';-
Identify long-running queries:
SELECT pid, now() - pg_stat_activity.query_start AS duration, query, state FROM pg_stat_activity WHERE state != 'idle' ORDER BY duration DESC LIMIT 10; -
Check if a specific service is hoarding connections:
SELECT application_name, count(*) FROM pg_stat_activity GROUP BY application_name ORDER BY count DESC;
Resolution
If a single query is blocking:
-- Cancel the query gracefully
SELECT pg_cancel_backend(<pid>);
-- If cancel doesn't work within 30 seconds, terminate
SELECT pg_terminate_backend(<pid>);If a service has too many connections:
- Check the service's pool configuration in
config/database.yml - Verify the replica count hasn't scaled beyond expected
- Restart the affected service:
kubectl rollout restart deployment/<service>
Escalation
If connections remain exhausted after 15 minutes, page the database on-call.
Runbooks work because they eliminate decision-making during incidents. Under stress, people follow checklists better than they reason from first principles.
## API Documentation
API documentation has two audiences: developers integrating with your API, and your future self debugging a production issue. OpenAPI specs serve both.
```yaml
# ❌ Documentation separate from code — drifts immediately
# docs/api.md
# POST /api/orders - Creates a new order
# Body: { items: [...], customerId: "..." }
# Returns: Order object
# ✅ OpenAPI spec validated against implementation
openapi: '3.0.3'
info:
title: Order Service API
version: '1.0.0'
paths:
/api/v1/orders:
post:
summary: Create a new order
operationId: createOrder
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [customerId, items]
properties:
customerId:
type: string
format: uuid
example: "550e8400-e29b-41d4-a716-446655440000"
items:
type: array
minItems: 1
items:
type: object
required: [productId, quantity]
properties:
productId:
type: string
quantity:
type: integer
minimum: 1
responses:
'201':
description: Order created
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
'400':
description: Validation error
'401':
description: Not authenticated// Validate API responses match the spec in tests
import SwaggerParser from '@apidevtools/swagger-parser';
describe('Order API', () => {
let spec: any;
beforeAll(async () => {
spec = await SwaggerParser.validate('./openapi.yaml');
});
it('POST /api/v1/orders matches spec', async () => {
const response = await request(app)
.post('/api/v1/orders')
.send({ customerId: 'cust-1', items: [{ productId: 'p1', quantity: 2 }] });
expect(response.status).toBe(201);
// Validate response shape matches OpenAPI schema
const schema = spec.paths['/api/v1/orders'].post.responses['201']
.content['application/json'].schema;
expect(() => validateSchema(response.body, schema)).not.toThrow();
});
});The API spec is tested against the actual implementation. When the code changes and the spec does not, the test fails. Documentation cannot drift.
README as a Gateway
The README is the first document anyone reads. It should answer three questions in under two minutes: what is this, how do I run it, how do I contribute.
# Order Service
Handles order creation, payment processing, and fulfillment tracking.
Part of the e-commerce platform.
## Quick Start
```bash
# Prerequisites: Node 20, PostgreSQL 14+, Redis 7+
cp .env.example .env
npm install
npm run db:migrate
npm run dev # http://localhost:3000
npm test # Run tests
```
## Architecture
See [docs/adr/](docs/adr/) for decision records.
| Component | Technology | Purpose |
|----------------|-----------|----------------------------|
| API | Express | HTTP endpoints |
| Database | PostgreSQL| Order storage |
| Cache | Redis | Session + rate limiting |
| Queue | BullMQ | Async payment processing |
## API Reference
Full API docs: `npm run docs` → http://localhost:3000/docs
## Common Tasks
| Task | Command |
|-------------------------|--------------------------|
| Run tests | `npm test` |
| Run specific test | `npm test -- --grep "orders"` |
| Generate migration | `npm run db:migration:create` |
| View API docs locally | `npm run docs` |
| Lint | `npm run lint` |No paragraphs of background. No installation guide for Git. Assume the reader is an engineer who can figure out prerequisites from the list. Get them to a running system as fast as possible.
Documentation-as-Code
Treat documentation like code: store it in version control, review it in pull requests, validate it in CI.
# .github/workflows/docs.yml
name: Documentation Checks
on: pull_request
jobs:
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Check for broken links
- name: Link checker
uses: lycheeverse/lychee-action@v1
with:
args: --verbose --no-progress 'docs/**/*.md' 'README.md'
# Validate OpenAPI spec
- name: Validate API spec
run: npx @redocly/cli lint openapi.yaml
# Ensure ADRs follow the template
- name: ADR format check
run: |
for file in docs/adr/adr-*.md; do
if ! grep -q "### Status" "$file"; then
echo "ERROR: $file missing Status section"
exit 1
fi
if ! grep -q "### Decision" "$file"; then
echo "ERROR: $file missing Decision section"
exit 1
fi
doneCI enforces documentation standards the same way it enforces code standards. Broken links, invalid API specs, and malformed ADRs fail the build.
Key Takeaways
- Write ADRs for every significant technical decision — future engineers need the "why", not just the "what"
- Create runbooks for operational tasks — step-by-step guides eliminate decision-making under stress
- Validate API docs against implementation — test that responses match OpenAPI schemas to prevent drift
- Keep READMEs actionable — answer "what, how to run, how to contribute" in under two minutes
- Store documentation next to code — review it in PRs, validate it in CI, version it alongside the source
- Automate freshness checks — broken link detection, schema validation, and format enforcement in CI


