Documentation Guidelines
Standards for documenting code at Unkey
The Problem
Documentation serves two masters: the engineer who writes it and the engineer who reads it six months later. Too little documentation leaves readers guessing. Too much buries the signal in noise. The goal is documentation that helps engineers understand and use code correctly. Nothing more, nothing less.
The principles in this guide apply to all languages. The examples are primarily in Go since that's most of our backend, but the philosophy (document the "why", match depth to complexity, don't restate the obvious) is universal.
Quick Checklist
Before submitting documentation, verify each item. This checklist catches the most common problems.
Accuracy
- Every claim in the documentation matches the actual code behavior
- Return value descriptions match what the code actually returns (not what you assume)
- Error conditions listed are actually possible and described correctly
- Default values mentioned match the actual defaults in the code
- Constraints documented (like "must be positive") are actually enforced, and you've noted when/how
Completeness
- Every exported symbol (function, type, constant, variable) has a doc comment
- Package has a
doc.goif it has non-trivial behavior - Non-obvious behavior is documented (edge cases, nil handling, concurrency)
- The "why" is explained for design choices that aren't self-evident
Quality
- Doc comments start with the symbol name ("Config holds..." not "This struct...")
- Uses prose, not bullet lists (unless enumerating truly parallel items)
- Depth matches complexity (one-liners for simple functions, paragraphs for complex ones)
- Cross-references use bracket syntax:
[TypeName],[FuncName] - No stale documentation from copy-paste or refactoring
Verification
- You've read the implementation, not just the function signature
- For functions returning values on failure, you've checked what value is actually returned
- For unmarshal/decode operations, you've verified whether partial values can be returned on error
- You've tested any examples compile and run correctly
Writing Style
Write naturally. Use prose for explanations, not bullet points. Documentation should read like it was written by a thoughtful colleague, not generated from a template.
Bullet lists and numbered lists have their place when you're enumerating genuinely parallel items (a list of error codes, a sequence of steps in an algorithm). But reaching for bullets by default creates documentation that's exhausting to read and hard to follow. When you find yourself writing a list of single-sentence bullets, ask whether a paragraph would communicate the same information more clearly.
This applies to engineering docs, code comments, and RFCs alike. Compare:
The prose version is shorter, easier to read, and communicates the same information. It also forces you to think about what actually matters rather than mechanically listing every step.
Document the "why", not the "what"
The code already shows what it does. Documentation should explain why it exists, why it works this way, and what could go wrong. Consider these two approaches:
The second version answers questions the code cannot: Why does this function exist? What's it used for? What could go wrong if I use it incorrectly?
Documenting Design Choices
The "why" applies to design patterns and API choices, not just complex algorithms. When you choose a functional options pattern, a builder, or an unusual signature, briefly explain the reasoning:
You don't need to justify every decision. Standard patterns like "returns error as last value" or "takes context as first parameter" don't need explanation. But when you've made a deliberate choice between reasonable alternatives, a sentence explaining why helps future maintainers understand the design.
Common design choices worth documenting:
- Why does this function panic instead of returning an error?
- Why are these objects pooled instead of allocated fresh?
- Why is data read eagerly into memory vs streamed?
- Why does this return a concrete type instead of an interface?
- Why is this field exported when it could be private?
A single sentence is usually enough. "Sessions are pooled to reduce GC pressure under high request volume" tells the reader everything they need.
Public API Documentation
Every exported function, type, constant, and variable must be documented. This is the contract you're making with users of your code. When someone runs go doc on your package, they should understand how to use it without reading the implementation.
The depth of documentation should match the complexity of what you're documenting. A simple getter needs one line. A distributed consensus algorithm needs paragraphs. Most functions fall somewhere in between.
Simple Functions
Most functions are straightforward and need only a clear explanation:
Complex Functions
Functions with distributed behavior, multiple error conditions, or subtle semantics need thorough documentation. Here's an example of what comprehensive documentation looks like:
Compare this to an insufficient alternative:
The short version doesn't explain the distributed coordination, error conditions, or the meaning of the bool return vs error return. For complex functions, sparse documentation leaves users guessing.
When to Include Specific Details
Parameters: Document them when the purpose isn't obvious from the name and type, or when there are constraints like "must be positive" or "should be stable across calls".
Return values: Explain if the return pattern is subtle (like bool success plus separate error), or if there are multiple success states. For functions that return a value plus an error, be precise about what value is returned on failure. Does it return the zero value? The last attempted value? A partial result? This is a common source of documentation bugs where the writer assumes "zero value on error" but the implementation does something different.
Error conditions: List specific errors only when callers need to handle them differently, or when they're not obvious from context. Generic "returns error on failure" is usually sufficient for simple cases.
Concurrency: Only document if the function or type is designed for concurrent use, or if it explicitly must not be used concurrently. Don't document concurrency for simple stateless functions.
Performance: Only mention if there are non-obvious characteristics that affect usage decisions, like "O(n²), use [AlternativeFunc] for large inputs" or "blocks until response received".
Context: Only document context behavior if it's non-standard, like using context values, having specific timeout behavior, or special cancellation semantics.
What Not to Document
Knowing what to leave out is as important as knowing what to include. Documentation that restates the obvious creates noise that obscures the signal.
Don't document implementation details in doc comments. Those belong in code comments inside the function. Don't explain that context is used for cancellation; that's what context always does. Don't mention O(1) performance unless it would be surprising. Don't say a function is "safe for concurrent use" unless the type is specifically designed for concurrent access, or conversely, warn if it's unsafe when that would be unexpected.
The reader is a competent Go engineer. Trust them to understand standard patterns.
Package Documentation
Every significant package should have a doc.go file containing only the package comment and package declaration. This is the first thing engineers see when they browse the code or run go doc, so it should orient them quickly.
The doc.go file should explain what the package does, why it exists, how it fits into the larger system, key concepts and terminology, basic usage examples, and cross-references to important types and functions.
Structure of doc.go
Use # headers to organize sections. Include a usage example that someone can adapt immediately:
Not every package needs this treatment. Tiny utility packages with one or two obvious functions can skip it. Internal packages that are implementation details rarely need extensive documentation. But any package with non-trivial behavior, multiple cooperating types, or non-obvious usage patterns should have a doc.go that explains the mental model.
Internal Code
Internal functions have different documentation needs. The audience is your teammates maintaining this code, not external users consuming an API. Here, the "why" matters even more than the "what".
The implementation will change over time, but the reasoning behind design decisions stays valuable. When a future engineer wonders "why didn't they just use linear backoff?", the comment answers before they waste time rediscovering the thundering herd problem.
Complex Algorithm Documentation
For complex internal logic, explain the approach and reasoning:
When implementing standards or RFCs, reference them explicitly. If an algorithm has a name, use it so readers can find external documentation.
Types and Interfaces
Type documentation should explain what the type represents and any constraints or invariants. For config structs, document fields that aren't self-explanatory from their names and types:
Interface documentation should focus on the contract. What guarantees must implementations provide? What can callers assume?
Error Documentation
Document sentinel errors with what they mean and when they occur:
Only list specific error conditions in function docs when callers need to handle them differently:
Constants and Variables
Document the purpose. Add reasoning only for non-obvious design choices:
Examples
Go has built-in support for example functions that appear in documentation and are compiled (so they won't go stale). Use them for non-trivial usage patterns:
Simple getters, setters, and straightforward functions don't need examples. Focus examples on complex workflows, non-obvious usage patterns, and common integration scenarios.
Test Documentation
Document test helpers and complex test scenarios so future maintainers understand the test's purpose:
Document What NOT to Do
Sometimes the most valuable documentation warns against common mistakes:
Highlight non-obvious behaviors and edge cases: nil input handling, concurrency hazards, silent failures, performance bottlenecks, and conditions where functions behave unexpectedly.
Verify Before You Document
The most dangerous documentation is confident and wrong. Before writing docs, read the implementation. Don't document based on what you think the code does, or what it should do, or what the function name suggests. Document what it actually does.
Common verification failures:
Return values on failure. A function DoWithResult[T](fn func() (T, error)) (T, error) might return the zero value of T on error, or it might return the last value from fn even when fn failed. The only way to know is to read the code. If you write "returns zero value on error" without checking, you might be lying to your users.
Partial population on unmarshal errors. This one catches people constantly: json.Unmarshal can partially populate a struct before encountering an error. If your function does var req T; json.Unmarshal(data, &req); return req, err, the returned req on error is NOT the zero value. It's whatever state json.Unmarshal left it in. Either document this accurately ("returns partially populated value on error") or check if the code actually returns a fresh zero value.
Constraint enforcement timing. If you document "attempts must be at least 1", clarify whether this is validated at construction time or at call time. Users need to know when they'll discover their mistake.
Default values. Don't assume defaults. Check the constructor or initialization code. Defaults change, and documentation that says "defaults to 5" when the code says "defaults to 3" causes debugging nightmares.
Context behavior. If a function takes a context, verify whether it actually respects cancellation, and how. Does it check before each attempt? Can it be interrupted mid-operation? Does cancellation leave state half-modified?
When documenting existing code you didn't write, be especially careful. Your mental model of what the code "should" do may not match reality.
Common Mistakes
Learning from bad examples is often more instructive than studying good ones:
Restating the signature adds no value. "Add adds a and b" tells us nothing we couldn't see. Instead, explain when you'd use this function or what could go wrong.
Documenting irrelevant details creates noise. Mentioning O(1) complexity, standard context behavior, or basic concurrency safety for simple functions obscures more important information.
Missing critical information is dangerous. A Delete function that cascades to related records, performs a soft delete, or is irreversible should say so. The reader needs to know before they call it.
Stale documentation is worse than no documentation. When code changes but comments don't, readers learn to distrust all documentation. If you change behavior, update the docs in the same commit.
Assuming instead of verifying is the root cause of many documentation bugs. It's easy to write "returns zero value on error" or "validates input" without checking whether that's true. The fix is simple: read the code before documenting it.
Go Conventions
Follow these conventions that Go tooling depends on:
Start doc comments with the name of the thing being documented: "Config holds..." not "This struct holds...". Use present tense: "Returns" not "Will return". Write complete sentences with proper capitalization and punctuation. Use active voice.
For cross-references, use Go's bracket syntax: [TypeName], [FuncName], or [pkg.Symbol] for items in other packages. These become hyperlinks in go doc and pkg.go.dev.
Reference RFCs or standards when implementing them. Document side effects and mutating behavior. Make documentation self-contained so readers don't have to jump around to understand a single function.
Deprecation
When deprecating an API, provide a clear migration path:
Keeping Documentation Alive
Documentation that lies is worse than no documentation. The code is the source of truth; documentation is a helpful guide that must stay synchronized.
Update documentation whenever you change function behavior, modify parameters, alter error conditions, or discover existing docs are wrong. When reviewing code, check that documentation still matches implementation. When reading documentation that seems wrong, verify against the code and fix it if needed.
The test for good documentation is simple: would this help someone unfamiliar with the code understand how to use it correctly? If yes, ship it. If no, revise until it does.