Technical Writing: Documentation People Actually Read
Practical techniques for writing documentation, ADRs, and technical specs that your team will actually read and maintain.

Documentation fails not because engineers cannot write, but because they write for the wrong audience at the wrong time. A 2000-word design doc written after the code is merged is archaeology. A README that explains what the project does but not how to run it is decoration. The gap between documentation that exists and documentation that helps is entirely about timing, structure, and empathy for the reader.
Good documentation answers a specific question for a specific person at a specific point in their workflow. Everything else is noise.
The Four Types of Documentation
Not all documentation serves the same purpose. Treating a tutorial like a reference guide (or vice versa) creates docs that fail at both jobs.
| Type | Purpose | Audience | Example |
|---|---|---|---|
| Tutorial | Learning-oriented | New team members | "Getting started with our API" |
| How-to | Task-oriented | Active practitioners | "How to add a new data source" |
| Reference | Information-oriented | Experienced users | API endpoint specifications |
| Explanation | Understanding-oriented | Decision makers | "Why we chose event sourcing" |
<!-- ❌ Mixes tutorial with reference — confusing for both audiences -->
# API Guide
To use our API, first get an API key from the dashboard.
POST /api/users accepts { name: string, email: string }...
Here's a fun history of how we built the API...
<!-- ✅ Separate documents for separate purposes -->
# Getting Started (tutorial)
# API Reference (reference)
# Architecture Decision: REST vs GraphQL (explanation)When you catch yourself switching between teaching and listing, split the document. Each type has a different reader with different needs.
Architecture Decision Records
ADRs are the highest-leverage documentation an engineering team can write. They capture the why behind decisions — the context, constraints, and alternatives that shaped the codebase.
# ADR-007: Use PostgreSQL for Primary Datastore
## Status
Accepted
## Context
We need a primary datastore for the user service.
Current load: ~500 writes/sec, ~5000 reads/sec.
Team has deep PostgreSQL experience. Two engineers
have DynamoDB experience.
## Decision
Use PostgreSQL 14 with read replicas.
## Alternatives Considered
- **DynamoDB**: Lower ops overhead but vendor lock-in.
Team experience gap would slow initial development.
- **MySQL**: Similar capabilities, but our tooling and
migration scripts assume PostgreSQL.
## Consequences
- Must manage connection pooling (PgBouncer)
- Read replicas add ops complexity
- Team can reuse existing migration patterns
- Avoids vendor lock-in in the data layerThe key sections are Context and Alternatives Considered. Without context, future engineers cannot judge whether the decision still applies. Without alternatives, they cannot evaluate trade-offs when circumstances change.
Writing Effective ADRs
// ❌ Vague context — doesn't help future decisions
interface BadADR {
context: "We needed a database";
decision: "We chose PostgreSQL";
// Missing: why not alternatives? What constraints existed?
}
// ✅ Specific context — enables revisiting the decision
interface GoodADR {
context: {
loadPattern: "500 writes/sec, 5000 reads/sec";
teamExperience: "3 senior engineers with PostgreSQL";
constraints: ["no vendor lock-in", "ACID required"];
timeline: "must ship in 6 weeks";
};
decision: "PostgreSQL 14 with read replicas";
alternatives: Array<{
option: string;
pros: string[];
cons: string[];
reason_rejected: string;
}>;
}Treat ADRs as immutable. When a decision is superseded, write a new ADR that references the old one. Never edit a past ADR — it is a historical record of what was known at the time.
README Structure That Works
Most READMEs are either empty or a wall of text. A good README answers five questions in order, and most developers only need the first three.
# Project Name
One sentence: what it does and who it's for.
## Quick Start
Three commands or fewer to go from clone to running.
## Development
How to run tests, lint, and build locally.
## Architecture
High-level overview: key directories, data flow,
external dependencies.
## Deployment
How to deploy. Environment variables. Infrastructure.# ❌ "Check the wiki for setup instructions"
# (The wiki is outdated, has broken links, and contradicts itself)
# ✅ Quick Start that actually works
git clone git@github.com:team/project.git
cd project
cp .env.example .env
docker compose up -dThe Quick Start section is the most important. If a new team member cannot go from git clone to a running application in under 10 minutes, the README has failed.
Writing Code Comments
Code comments should explain why, not what. The code already explains what it does. A comment that restates the code adds noise. A comment that explains the reasoning becomes invaluable during debugging.
// ❌ Restates the code — adds nothing
// Increment counter by 1
counter += 1;
// ✅ Explains regulatory/business context
// GDPR Article 17: must purge all PII within 30 days
// of deletion request. The grace period allows undo.
const PURGE_DELAY_DAYS = 30;
// ✅ Explains non-obvious technical decisions
// Using requestAnimationFrame instead of setTimeout
// because setTimeout(0) can be throttled to 4ms+ in
// background tabs (Chrome 88+), causing visible jank
// when the tab regains focus.
requestAnimationFrame(flushUpdates);The best comments answer the question "why would someone change this, and what would they need to know?" If the answer is "nothing — it's obvious," skip the comment.
Inline Documentation in APIs
API documentation lives closest to the code when it is generated from the code. JSDoc, TypeDoc, or OpenAPI annotations ensure documentation stays in sync with the implementation.
/**
* Creates a new user account and sends a verification email.
*
* @param input - User registration data
* @returns The created user (without password hash)
* @throws {ConflictError} If email is already registered
* @throws {ValidationError} If input fails schema validation
*
* @example
* const user = await createUser({
* email: 'dev@example.com',
* name: 'Jane Doe',
* password: 'securePassword123'
* });
*/
async function createUser(input: CreateUserInput): Promise<User> {
const existing = await db.user.findByEmail(input.email);
if (existing) throw new ConflictError('Email already registered');
const hashed = await hashPassword(input.password);
const user = await db.user.create({ ...input, password: hashed });
await emailService.sendVerification(user.email, user.verificationToken);
const { password, ...safeUser } = user;
return safeUser;
}The @throws annotations are especially valuable — they document the error contract that callers must handle. Missing these leads to unhandled error types leaking to end users.
Keeping Documentation Alive
Documentation rots faster than code. The only defense is making documentation part of the workflow, not an afterthought.
Strategies that work:
- ADRs are required for any decision that affects more than one team
- README changes are part of the PR checklist for infrastructure changes
- Generated API docs run in CI — broken docs break the build
- Quarterly doc review: delete anything that is wrong (outdated docs are worse than no docs)
Strategies that fail:
- "Documentation sprints" — the backlog is infinite and the results are immediately stale
- Separate wiki maintained by a "documentation champion" — single point of failure
- Mandating comments on every function — produces noise, not signal
Key Takeaways
- Know your document type — tutorials, how-tos, references, and explanations serve different readers
- ADRs are the highest-leverage documentation — capture the why, not just the what
- READMEs must have a working Quick Start — if setup takes more than 10 minutes, the docs failed
- Comments explain why, not what — the code already shows what it does
- Automate what you can — generated API docs and CI checks prevent documentation rot
- Delete outdated docs — wrong documentation is worse than no documentation


