Technical Documentation Strategy: Docs as Code
A practical guide to treating documentation as code: versioning, review workflows, structure, and tools that keep docs accurate, discoverable, and maintainable.
Introduction
Few activities in software engineering pay off as reliably as documentation. A well-documented system shortens onboarding, prevents the same incident from being debugged twice, and preserves context when a team rotates. Treating docs as code means applying the same rigor you apply to source code — version control, code review, automated checks — to the words that describe how the system works.
I wrote this guide for engineering leads, platform teams, and senior developers who own a service or a platform and want a documentation system that doesn’t rot. It covers the four documents every service needs, the docs-as-code workflow, the tools that make it practical, and the metrics that tell you whether the system is working. What it deliberately skips is API reference generation in depth — that’s a separate concern handled by OpenAPI tooling.
When to Use Docs as Code
Adopt docs as code when at least one of these is true:
- More than one team touches the same service. A single author can keep a wiki in their head; two teams can’t. Git history and review make disagreements visible.
- Onboarding takes more than a day. If new hires spend a week asking “where is the runbook?”, the documentation system is the bottleneck.
- Incidents repeat with the same root cause. A runbook that lives next to the code gets updated in the same PR that fixes the bug; a runbook in a separate wiki doesn’t.
- You ship several times per week. Fast deploys mean docs drift fast; coupling doc updates to code changes is the only realistic way to keep them current.
- You need to prove compliance or audit trail. Regulated environments (SOC 2, ISO 27001, HIPAA) benefit from versioned, reviewed documentation with a clear owner.
Do not adopt the full system when:
- The project is a single-maintainer prototype with no external consumers. A README is enough; the overhead of ADRs and runbooks will slow you down.
- There’s no CI pipeline on the team and no plan to add one. Docs-as-code depends on automation catching what humans miss; without link checking and linting in CI, it rots just as fast as a wiki does.
- Documentation ownership is unclear. If no one is accountable for the docs, the workflow won’t fix that — it will only move the graveyard from Confluence to Git.
The Four Essential Documents
Four documents cover almost everything a service needs to explain. They answer different questions at different depths, and they’ve got different lifecycles — which is why a single “documentation” folder rarely works.
| Document | Answers | Audience | Update Frequency |
|---|---|---|---|
| README | What is this? How do I run it? | New developers, users | Every major change |
| Architecture Decision Record (ADR) | Why did we choose this? | Current and future maintainers | Once per major decision |
| Runbook | How do I fix it when it breaks? | On-call engineers | After every incident |
| API Reference | What does this endpoint do? | API consumers | Auto-generated from code |
The README changes with the code. The ADR is append-only — you write it once and only update the status. The runbook changes after every incident, not on a schedule. The API reference should never be hand-written; if it’s, it’s already wrong.
Docs as Code Workflow
The workflow is the same as a code change, with one extra review pass for accuracy.
The key property is that the documentation change and the code change live in the same pull request. If they’re split across two repos or two PRs, one of them will ship without the other.
Why It Works
| Principle | How Docs as Code Applies It |
|---|---|
| Version control | Git history shows when docs changed and why — git blame on a paragraph is more honest than a “last reviewed” date. |
| Code review | Reviews catch technical inaccuracies, not just typos. A reviewer who understands the code can reject a doc that contradicts it. |
| Automation | CI catches broken links, stale anchors, and formatting drift before they reach production. |
| Branching | Doc changes ship with the code they describe, so a feature branch is self-contained. |
README Structure
A good README answers these questions in the order a newcomer actually asks them:
# Service Name
One-line description of what this service does.
## Quick Start
How to run it locally in under 5 minutes.
## Architecture Overview
High-level diagram and key dependencies.
## Configuration
Required environment variables with examples.
## Testing
How to run unit, integration, and E2E tests.
## Deployment
How this service is deployed (CI/CD link, environment list).
## Troubleshooting
Common errors and how to resolve them.
## Contributing
Link to contribution guidelines and code of conduct.
Architecture Decision Records (ADRs)
ADRs exist to capture the context and consequences of a major technical decision at the moment it’s made. Six months later, when someone asks “why did we do it this way?”, the ADR answers for you — no debate needed.
ADR Template
# ADR-042: Adopt Kafka for Event Streaming
## Status
Accepted
## Context
The order service needs to publish events to 4 downstream consumers. REST callbacks are unreliable and create tight coupling.
## Decision
Adopt Apache Kafka as the event streaming platform.
## Consequences
### Positive
- Decouples producers from consumers
- Supports replay for new consumers and debugging
- Handles backpressure via consumer lag
### Negative
- Operational complexity (ZooKeeper, brokers, partitions). See [event-driven architecture](/guides/event-driven-architecture-guide/).
- Team needs to learn [event-driven patterns](/guides/event-driven-architecture-guide/)
- Eventual consistency requires Saga pattern for some flows
Rule of thumb: Write an ADR for any decision that costs > 2 weeks to reverse.
Runbooks
A runbook is a step-by-step guide for responding to a known alert or failure mode.
Good Runbook Structure
# Runbook: Database Connection Pool Exhausted
## Symptoms
- Alert: `db_pool_connections_exhausted`
- User impact: API requests timeout after 5 seconds
## Diagnosis
1. Check current pool usage: `SELECT count(*) FROM pg_stat_activity;`
2. Identify slow queries: `SELECT * FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10;`
3. Check for connection leaks in application logs
## Resolution
1. If caused by slow query: kill query, [add index](/recipes/database-indexing/), or scale [read replicas](/guides/database-design-guide/)
2. If caused by connection leak: restart app pods (temporary), then deploy fix
3. If persistent: increase pool size in config and redeploy
## Escalation
If resolution fails after 15 minutes, escalate to Database Team on-call.
Documentation Tools
The tooling stack matters less than the workflow, but the wrong stack adds friction. Pick tools that fit the doc type, not one tool for everything.
| Tool | Best for | Pros | Cons |
|---|---|---|---|
| Markdown in Git | READMEs, ADRs, runbooks | Universal, versioned, free, reviewable | No built-in search; you bring your own |
| MkDocs Material | Internal engineering docs | Fast, Python-based, great search, versioning | Theming is limited vs Docusaurus |
| Docusaurus | Product docs with React components | Rich theming, versioning, plugin ecosystem | Heavier build, Node-only |
| Backstage | Service catalog + docs portal | Centralizes service ownership, plugins | Significant setup; overkill for < 20 services |
| Notion / Confluence | Cross-team knowledge, process docs | WYSIWYG, easy for non-engineers | No git history; ownership rots silently |
| Swagger / OpenAPI | API reference | Auto-generated from code, always in sync | Limited to API surface; no narrative |
| Mermaid / PlantUML | Diagrams as code | Versioned, diffable, reviewable in PRs | Learning curve; complex diagrams get verbose |
Choosing the Right Stack
A common mistake is picking one tool for all four doc types. The stack that works for a README (Markdown in the repo) is wrong for a cross-team onboarding guide (Notion), and the tool that works for API reference (OpenAPI) is wrong for architecture explanation (long-form Markdown with Mermaid diagrams).
A pragmatic split for a mid-size platform:
- Code-adjacent docs (README, ADR, runbook): Markdown in the service repo. No exceptions. These docs change with the code and need the same review trail.
- Service catalog and discovery: Backstage if you’ve got more than 20 services; otherwise a single
OWNERS.mdindex in the monorepo is enough. - API reference: OpenAPI spec checked into the repo, rendered by the docs site. Never hand-written.
- Cross-team knowledge (onboarding flows, HR processes, runbooks for shared platforms): Notion or Confluence, with a named owner per page and a quarterly stale-review job.
- Diagrams: Mermaid for flowcharts and sequence diagrams, PlantUML for deployment topologies. Both render to SVG at build time and stay diffable.
Here’s the test I’d apply to any tool in the stack: hand it to a new hire. If they can’t find the doc they need in under five minutes, the tool is the wrong choice — no matter how good the content inside.
Documentation Metrics
A docs-as-code system without metrics is a wiki with extra steps. Track a small set of leading and lagging indicators so you can tell whether the system is working.
| Metric | Type | Target | How to measure |
|---|---|---|---|
| % services with a README | Coverage | 100% | Script that walks the service catalog and checks for README.md |
| % services with a runbook | Coverage | 100% | Same script, checks for runbook.md or a runbook entry in the catalog |
| Docs updated in the same PR as code | Freshness | > 90% | Diff heuristic: PRs that touch src/ should also touch docs/ or *.md |
| Onboarding time to first PR | Outcome | < 1 day | Self-reported by new hires in a 2-week check-in |
| Searches with zero results | Discoverability | < 10% | Search analytics from MkDocs/Docusaurus/Backstage |
| ADRs per service per quarter | Decision velocity | 1-3 | Count of files added under adr/ |
| Runbooks triggered and resolved without escalation | Runbook quality | Rising | On-call post-incident review |
Skip vanity metrics like “total word count” or “number of pages”; they grow monotonically while telling you nothing about whether anyone reads the docs. If you only track two, pick onboarding time and searches with no result; both measure the only thing that matters, whether a new reader finds what they need.
What Works
- Write the README first — if you can’t explain how to run the service, the service isn’t ready to ship.
- Keep docs next to the code — docs in a separate repo rot faster than the code they describe.
- Automate link checking in CI — broken links destroy trust; CI should fail the build on them, not warn.
- Use diagrams as code — Mermaid and PlantUML keep diagrams versioned, reviewable, and diffable.
- Review docs in PRs — a code change without a doc change is an incomplete PR; make it a merge check.
- Set a freshness policy — flag docs not updated in 12 months for review, and delete the ones no one owns.
Common Mistakes
- Writing docs only for beginners. Experts need docs too — API references, architecture overviews, and ADRs are where senior engineers actually spend their reading time.
- Creating a wiki graveyard. Wikis become outdated because there’s no review process and no owner. Can’t name the owner of a page? That page is already dead — it just hasn’t been deleted yet.
- Documenting what, not why. The “what” is in the code. The “why” — why Kafka over RabbitMQ, why Postgres over MySQL — is what you forget in six months and what an ADR exists to preserve.
- Over-documenting. If the code is self-explanatory, don’t restate it in prose; explain the intent and the constraints the code doesn’t show.
- Separating docs and code in different repos. The friction of context switching guarantees the docs won’t be updated. One repo, one PR, one review.
Documentation Ownership Model
A docs-as-code workflow handles the mechanics — versioning, review, publishing. It doesn’t solve ownership. Without an owner, every doc eventually becomes orphaned, and orphaned docs rot.
The CODEOWNERS approach
Git already solved this problem for code review: a CODEOWNERS file in each repo declares who is responsible for which paths. Apply the same idea to docs.
# .github/CODEOWNERS
# README and architecture docs are owned by the service team
/README.md @payment-team
/architecture.md @payment-team
# Runbooks are owned by the on-call rotation
/runbook.md @payment-oncall
# ADRs are owned by the tech lead
/adr/ @payment-tech-lead
This gives you three properties:
- A PR that touches
runbook.mdautomatically requests a review from the on-call rotation, not the whole team. - A stale-docs job can tag the owners directly instead of opening a generic issue.
- When a team reorganizes, the
CODEOWNERSfile is the single place to update ownership.
What to do with orphaned docs
Once a quarter, run a script listing markdown files nobody has touched in 12 months. For each:
- If it’s got an owner in
CODEOWNERS, open an issue asking the owner to review or delete. - If it’s got no owner, post it in the team channel and give the team two weeks to claim it.
- When no one claims it, delete it. An unowned doc is worse than none at all — it looks authoritative and it’s wrong.
Deleting docs feels destructive, but it’s the single most impactful maintenance action you can take. A deleted doc stops misleading readers; an unowned doc keeps misleading them until someone files a bug that no one picks up.
Advanced Topics
Scenario: Documentation System for a Platform
The Diataxis framework separates documentation into four types by purpose, not by topic. Of the structures I’ve seen hold up on platforms with many teams, this is the one that scales best.
| Type | Purpose | Audience | Example |
|---|---|---|---|
| Tutorial | Learn | Novices | ”Create your first service” |
| How-to | Solve | Practitioners | ”How to configure Redis” |
| Reference | Consult | Everyone | API docs, CLI flags |
| Explanation | Understand | Curious | ”Why we use Kafka” |
A Backstage service catalog maps each service to its docs by type:
services/
payment-service/
README.md # How-to: how to run locally
api.yaml # Reference: OpenAPI spec
runbook.md # How-to: what to do when it breaks
architecture.md # Explanation: design decisions
on-call.md # How-to: on-call procedures
order-service/
...
tutorials/
new-microservice.md # Tutorial: step by step
new-endpoint.md
local-dev-setup.md
adr/
001-record-events.md
002-use-postgresql.md
003-adopt-kafka.md
A runbook in this structure is a how-to doc keyed by symptom, not by component:
# Runbook: Payment Service
## Symptom: Error rate > 1%
1. Check the error dashboard for the top failing endpoint.
2. Check the last deploy: `kubectl rollout history deployment/payment`.
3. If a deploy happened in the last 30 minutes, roll it back.
4. Otherwise, check logs in Loki: `{service="payment"} |= "error"`.
5. Check the database: active connections, long-running queries, locks.
6. If the database is saturated, escalate to the DBA on-call.
7. Open a post-mortem within 48 hours regardless of outcome.
## Symptom: p99 latency > 500ms
1. Open the trace in Jaeger and find the slowest span.
2. If the slow span is a database query, run `EXPLAIN ANALYZE` on it.
3. If the slow span is an external API call, check the provider's status page.
4. If the slow span is CPU-bound, check whether HPA is scaling and why.
5. Open a post-mortem if the incident lasted more than 10 minutes.
The lessons from platforms that have done this at scale:
- Diataxis separates doc types by purpose, so a reader knows whether they’re learning, solving, consulting, or understanding — and the doc is written for that intent.
- Runbooks are living docs: update them after every incident, not on a quarterly schedule. A runbook that wasn’t touched in the last incident is probably wrong.
- ADRs preserve the “why” behind decisions; without them, the team re-litigates the same argument every six months.
- Backstage centralizes the service catalog and links each service to its docs, so discovery isn’t “ask someone in Slack”.
- Measure doc quality the way you measure code quality — with metrics, owners, and review.
Frequently Asked Questions
Who should write documentation?
The engineer who built the feature — nobody else has the context, and a technical writer can't reconstruct it after the fact. Technical writers can polish structure, tone, and consistency, but the source of truth must come from the implementer. Make doc writing part of the Definition of Done so it ships with the code, not after it.
How do I keep docs from becoming outdated?
Treat outdated docs as a bug, not a chore. Add a documentation label to your bug tracker and prioritize it alongside code bugs. Require README updates in the same PR as code changes — a CI check that fails when src/ changes but no *.md changes is the cheapest enforcement. Run a stale-docs job that flags any markdown file untouched in 12 months and assigns it to its owner for review.
Should we use Confluence or Markdown in Git?
Honestly? Both, but for different purposes. Git-based Markdown for code-adjacent docs (READMEs, ADRs, runbooks) that change with the code and need a review trail. Confluence or Notion for cross-team knowledge, onboarding, and process docs that evolve independently of any single repo. The failure mode is mixing the two: putting runbooks in Confluence where the on-call engineer can't find them at 3 a.m., or putting HR onboarding flows in Git where non-engineers can't edit them.
How do I version documentation with releases?
Tag docs with the same version as the code. In a monorepo, keep a docs/ folder per major version or use a version frontmatter field that the docs site uses to build versioned selectors. In polyrepo, branch docs alongside code — a release/2.x branch ships docs/2.x. Never maintain a single "current" docs site for software with several supported versions in production; users on 2.x will follow 3.x instructions and break their setup.
What belongs in an ADR and what does not?
An ADR records the context, the decision, and the consequences of a significant technical choice. It does not record implementation details, step-by-step instructions, or opinions without consequences. A good test: if the decision were reversed, would a future maintainer need to understand why it was made? When the answer is yes, that decision earns an ADR. When the honest answer is "they can just read the code", don't write the ADR. Use the ADR template as a starting point.
How do I measure if documentation is actually useful?
Track onboarding time (ask new hires how long it took to make their first PR) and search-zero-results rate (what people looked for and didn't find). Both measure whether a reader can find what they need, which is the only outcome that matters. Avoid vanity metrics like total word count or page views — they go up forever and tell you nothing about quality.
Related Resources
What Works in Code Review — For Authors and Reviewers
A practical guide to useful code reviews: how to write reviewable code, give constructive feedback, and keep reviews fast and focused.
GuideGit Branching Strategies Compared (GitFlow, GitHub, Trunk)
Trunk-based development, GitFlow, GitHub Flow, and GitLab Flow compared — with a decision framework for team size, release cadence, and CI/CD maturity.
DocADR Template
A reusable template for Architecture Decision Records that capture context, decision, and consequences.
DocRunbook Template
A reusable template for operational runbooks: incident response, deployment procedures, and routine tasks.
GuideOn-Call and Incident Response Playbook
A practical playbook for on-call engineers: triage, escalation, communication, and postmortems. Reduce MTTR and build a resilient incident response culture.