StackPractices
beginner By Mathias Paulenko

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.

Topics: devops

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.

DocumentAnswersAudienceUpdate Frequency
READMEWhat is this? How do I run it?New developers, usersEvery major change
Architecture Decision Record (ADR)Why did we choose this?Current and future maintainersOnce per major decision
RunbookHow do I fix it when it breaks?On-call engineersAfter every incident
API ReferenceWhat does this endpoint do?API consumersAuto-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.

Docs-as-code workflow — author writes Markdown docs, opens a PR, reviewer checks code and docs, CI runs lint/link/spelling checks, merge publishes docs automatically

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

PrincipleHow Docs as Code Applies It
Version controlGit history shows when docs changed and why — git blame on a paragraph is more honest than a “last reviewed” date.
Code reviewReviews catch technical inaccuracies, not just typos. A reviewer who understands the code can reject a doc that contradicts it.
AutomationCI catches broken links, stale anchors, and formatting drift before they reach production.
BranchingDoc 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.

ToolBest forProsCons
Markdown in GitREADMEs, ADRs, runbooksUniversal, versioned, free, reviewableNo built-in search; you bring your own
MkDocs MaterialInternal engineering docsFast, Python-based, great search, versioningTheming is limited vs Docusaurus
DocusaurusProduct docs with React componentsRich theming, versioning, plugin ecosystemHeavier build, Node-only
BackstageService catalog + docs portalCentralizes service ownership, pluginsSignificant setup; overkill for < 20 services
Notion / ConfluenceCross-team knowledge, process docsWYSIWYG, easy for non-engineersNo git history; ownership rots silently
Swagger / OpenAPIAPI referenceAuto-generated from code, always in syncLimited to API surface; no narrative
Mermaid / PlantUMLDiagrams as codeVersioned, diffable, reviewable in PRsLearning 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.md index 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.

MetricTypeTargetHow to measure
% services with a READMECoverage100%Script that walks the service catalog and checks for README.md
% services with a runbookCoverage100%Same script, checks for runbook.md or a runbook entry in the catalog
Docs updated in the same PR as codeFreshness> 90%Diff heuristic: PRs that touch src/ should also touch docs/ or *.md
Onboarding time to first PROutcome< 1 daySelf-reported by new hires in a 2-week check-in
Searches with zero resultsDiscoverability< 10%Search analytics from MkDocs/Docusaurus/Backstage
ADRs per service per quarterDecision velocity1-3Count of files added under adr/
Runbooks triggered and resolved without escalationRunbook qualityRisingOn-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.md automatically 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 CODEOWNERS file 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:

  1. If it’s got an owner in CODEOWNERS, open an issue asking the owner to review or delete.
  2. If it’s got no owner, post it in the team channel and give the team two weeks to claim it.
  3. 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.

TypePurposeAudienceExample
TutorialLearnNovices”Create your first service”
How-toSolvePractitioners”How to configure Redis”
ReferenceConsultEveryoneAPI docs, CLI flags
ExplanationUnderstandCurious”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.