Maintenance Matters: Documentation

Solomon Hawk, Senior Developer

Article Categories: #Code, #Front-end Engineering, #Back-end Engineering

Posted on

In this latest entry of our Maintenance Matters series, we'll discuss Documentation: what, why, and how.

This article is part of a series focusing on how developers can center and streamline software maintenance. The other articles in the Maintenance Matters series are: Continuous IntegrationCode Coverage, Default Formatting, Building Helpful Logs, Timely UpgradesCode Reviews, Good Tests, and Monitoring.

Introduction #

Good documentation is critical for maintainable software projects. It facilitates onboarding new engineers and makes ongoing contributions easier. A project with poor documentation will suffer over the long term inevitably becoming harder and harder to maintain, improve, and iterate on.

The software development lifecycle is continuous. Maintainability is a measure of how sustainable changes over time are for a given project. Documentation is a small but important piece of that story. For additional considerations, have a look at our ongoing series on why Maintenance Matters.

How to think about documentation #

Writing good documentation starts with a mindset that enables engineers to make the right decisions about when and what to document, and how to go about capturing and conveying the right information.

Documentation is for people.

All good writing recognizes its audience. Documentation usually falls into one of two broad categories: User Documentation and Developer Documentation.

Writing for users should be:

  1. Approachable - uses a friendly tone, is easy to navigate and understand
  2. Thorough - does not omit information, is generous in detail and explanation
  3. Connected - needs to be smart, pages should be connected contextually and help guide a user to the next logical thing

Writing for engineers should be:

  1. Straightforward - omits unnecessary information, is direct and specific, sparse but not lacking
  2. Technical - uses domain-specific language, is pedantic, and where necessary, links off to relevant resources
  3. Organized - structured and easy to navigate

The rest of this article focuses on Developer Documentation which is the primary concern for the types of software projects that Viget typically engages with.

What counts as documentation #

Documentation in software projects encompasses a variety of different things including:

  1. README.mds and CONTRIBUTING.mds
  2. Code Comments
  3. Automated Tests
  4. Pull Request descriptions
  5. Issue descriptions for new features or bug reports
  6. Pull Request and Issue Templates
  7. Commit Messages
  8. Code Reviews
  9. Supporting domain or process-specific documents

Each of these should be regarded thoughtfully and with care for future engineers who’s contributions will shape the maintainability of the project.

Which things should be documented #

  1. Project prerequisites and software requirements
    An exhaustive list of all the required programs that must be installed on the system which aren’t dependencies managed by the project’s package manager

  2. Local development setup instructions
    Step by step, what to do to get the project up and running. It’s great to include copy+paste-able commands and snippets here.

  3. Where to find Secrets & Required Credentials
    Any sensitive information that isn’t checked into version control but is required to run the application. If you have direct links to secrets or commands to run to fetch them, this is the place to capture that information.

  4. Deployment: ops processes, how to deploy, information about remote environments
    This is especially important for new contributors who need to get their changes pushed to remote environments.

  5. Contribution guidelines
    Information about code style, linting/formatting, commit style, steps for creating good issues and pull requests, external documentation, code of conduct, and other project processes. CONTRIBUTING.md is a good place for these.

  6. Tricky, weird, or unexpected code
    There are valid reasons for needing to write code that may not be super easy to understand for other contributors or even yourself a year from now. These areas are ripe for detailed code comments or commit messages that explain their reason for existing.

  7. Testing methodology
    Notes about expectations, coverage thresholds, test isolation, mocking, fixtures, seeding, and anything else to help contributors write and maintain properly tested code.

  8. Complex application behaviors
    One of the best ways to express this information is in the form of tests. Future contributors will be grateful to have assertions about valid and invalid program states and how to get into (or avoid) them.

  9. Technical debt
    It can be useful to organize thoughts and expectations regarding technical debt. How it is going to be tracked (e.g. how to write good code comments related to technical debt, whether to create issues for future refactoring)? When and how will it be repaid (e.g. project/team processes for allocating time to address a backlog of technical debt)?

This list isn’t exhaustive, but it should be a good starting point when considering what kinds of things deserve deliberate consideration.

Creating maintainable documentation #

Here are a few things to keep in mind when iterating on a project’s documentation in order to make it easier to contribute to over time. Documentation should be…

Editable - easy to add to or amend
Keeping the structure of your documentation as simple as possible will make it easier to know where to add or change information. Organize information hierarchically and use clear headings.

Findable - easy to locate relevant information for a given task
Co-locating documentation with the relevant parts of a project can make it easier to know where to look. Avoid putting everything in a single place; prefer multiple files as close to their relevant project scope as possible.

Removable - easy to remove, when no longer relevant
In keeping with the previous point, co-located documentation is more easily removed when deleting features. It’s typically more difficult to remember to look somewhere else for a portion of a file that’s relevant to some specific deleted code or feature.

Maintained - kept up to date
Circular logic aside, documentation needs to be curated over time. This means accounting for the time it takes to update in task estimates and planning, checking that documentation has been properly updated in code review, and regularly auditing documentation to ensure correctness.

Documentation written in Markdown and checked into version control facilitates easy editing and discovery.

Be mindful that… #

Code Comments are code
Engineers dedicate time and attention to reading comments while working on code. Effort should be taken to minimize the amount of unnecessary information conveyed in code comments. Extraneous comments can distract from and dilute the impact of important comments.

Good code leads to good documentation
It’s great when code is self-documenting and requires no additional information to be communicated but sometimes there are requirements that necessitate a certain approach which might not be obvious to new contributors. In those cases it’s useful to add a comment documenting the “WHY” behind a particular strategy or decision.

Outdated documentation can be worse than no documentation at all
It takes engineers time and effort to read and parse documentation. When that information is wrong it can be confusing and lead to extra cycles spent determining what to do: does the code need to be updated or the documentation?

Not all documentation is about code
It's important to document supporting aspects of a project. For example: error monitoring process/procedures, product ownership & decision-making, bug-tracking & issue management, and quality assurance information.

Documentation is a great place to start for new contributors #

New contributors are well-positioned to contribute to the maintainability of a project’s documentation. It’s convenient to audit the setup instructions, project requirements, contribution guidelines, and required credentials while initially setting up a project locally. Contributing edits to this documentation as you encounter inaccurate, outdated, or missing information is a great way to pitch in while getting ramped up.

Language and tooling support #

Don’t reinvent the wheel. Lean into the conventions and standard approaches for documenting code in whichever language you’re using. Chances are there’s a broad community of engineers who have over time distilled a set of common best practices that you should leverage.

Not all languages are equal, some treat documentation as a first-class concern. Use the language-specific tools and idioms to document your work.

Use standard tooling, whenever possible
Using automated tooling to maintain accessible documentation can save time and standardizing your approach using language-specific tools and idiomatic approaches makes maintaining and updating documentation smoother.

Shoutout: Elixir

José Valim deserves recognition for building language-level support for first-class documentation in Elixir via @moduledoc and @doc. Code comments can contain executable example code. Mix, the Elixir build tool, generates a full-fledged HTML-based documentation site based on code comments, simply by running mix docs . DocTest allows you to executes the code examples from your test suite ensuring they aren’t invalid our outdated.

The Elixir page on Writing Documentation make an interesting distinction:

”Documentation is an explicit contract between you and users of your Application Programming Interface (API), be them third-party developers, co-workers, or your future self. Modules and functions must always be documented if they are part of your API.

Code comments are aimed at developers reading the code.”

Here’s an example of what that looks like in practice.

For a given Elixir module you annotate the module and each function individually, optionally including example invocations and their associated output:

defmodule GameApp.Game do
  @moduledoc """
  `GameApp.Game` defines a struct that encapsulates game state as well as many
  functions that advance the game state based on actions that players can take.
  """

  # ... module attributes, typedefs, imports, aliases and struct info omitted for brevity ...

  @doc """
  Creates a game.

  ## Examples

      iex> Game.create(shortcode: "ABCD", creator: Player.create(id: "1", name: "Gamer"))
      %Game{
        shortcode: "ABCD",
        creator: %Player{id: "1", name: "Gamer"},
        players: %{"1" => %Player{id: "1", name: "Gamer"}},
        scores: %{"1" => 0},
        funmaster: %Player{id: "1", name: "Gamer"},
        funmaster_order: ["1"]
      }

  """
  @spec create(keyword()) :: Game.t()
  def create(attrs \\ []) do
    struct(Game, Enum.into(attrs, %{config: %GameConfig{}}))
    |> player_join(Keyword.get(attrs, :creator))
  end

After running mix docs, you get a docs folder with a bunch of generated .html files, some fonts, styles and JavaScript. You can then host these files anywhere and browse the docs site generated entirely from comments in your app. The docs for the above code sample live here.

Then you call the doctest/1 macro in your test and ExUnit will also execute the code written in your @doc comments if it's prefixed by iex>.

defmodule GameTest do
  use ExUnit.Case, async: true

  alias GameApp.{Round, Game, Player}
  alias GameApp.Config, as: GameConfig

  ##################################
  doctest GameApp.Game, import: true
  ##################################

  @shortcode "ABCD"
  @player1 Player.create(id: "1", name: "Gamer")

  setup do
    [game: Game.create(shortcode: @shortcode, creator: @player1)]
  end

  test "create/2", %{game: game} do
    # ...
  end

  # ...
end

Conclusion #

Documentation is an important chapter in the tale of Building Maintainable Applications. As engineers it is our responsibility to spend adequate time documenting our work, check that other contributors are maintaining high standards for documentation, and advocate for protected time to ensure documentation stays relevant, up-to-date and complete.

Reference Examples #

Included below are some examples of effective and well-organized documents you might use as a starting point for establishing strong documentation practices in your own projects.

README.md:

# Project Name

A short description of the product and tech stack.

## Table of Contents

- [Links](#links)
- [Prerequisites](#prerequisites)
- [Setup](#setup)
  - [Docker](#docker)
- [Git Workflow](#git-workflow)
- [Local Development](#local-development)
  - [Assets](#assets)
  - [Database](#database)
  - [Browser & Device Testing](#browser-and-device-testing)
- [Deployment](#deployment)
  - [Provisioning & Hosting](#provisioning-and-hosting)
  - [Automated Releases](#automated-releases)
  - [Production Releases](#production-releases)
- [Testing](#testing)

## Links

- https://example.com - The production site
- https://staging.example.com - The staging site
- https://integration.example.com - The integration site
- https://github.com/org/repo - GitHub
- https://github.com/org/repo/issues - Ticketing

## Prerequisites

Here's a list of the programs you need installed in order to run the app on your local machine (the following are just examples):

For managing local installations for multiple versions of system dependencies, we recommend using [asdf](https://asdf-vm.com/) version manager.

| Dependency | Notes |
| ---------- | ----- |
| **python 3.9** | on macOS install via [homebrew](https://brew.sh/) [package](https://formulae.brew.sh/formula/python@3.9#default) |
| **[Node](http://nodejs.org/) 16.17.1+ & [npm](https://www.npmjs.com/) 8.1.2+** ||

## Setup

Steps to get the project running locally. This might include running an executable script or in some cases a link to external documentation (e.g. for a Craft project) that contains more detailed steps, filling in configuration files, migrating and seeding the database, and finally starting up the services.

**Run the setup script** (ensures the host machine is capable of running the application):

    ```bash
    $ bin/setup
    ```

**Run database migrations**:

    ```bash
    $ bin/migrate
    ```

**Seed the database**:

    ```bash
    $ bin/seed
    ```

**Start the server**:

    ```bash
    $ bin/server
    ```

### Docker

This app supports being run inside [Docker](https://www.docker.com/) via `docker compose`.

1. Install [Docker Desktop for Mac](https://docs.docker.com/docker-for-mac/install/).

2. Build the images (might take awhile the first time):

    ```bash
    $ docker compose build
    ```

3. Spin up the containers locally:

    ```bash
    $ docker compose up
    ```

## Git Workflow

## Local Development
### Assets
### Database
### Browser & Device Testing
## Deployment
### Provisioning & Hosting
### Automated Releases
### Production Releases
## Testing

.github/PULL_REQUEST_TEMPLATE/default.md:

Pull Request templates are useful for ensuring consistent, high quality contributions that are easy to review and merge.

---
name: Pull Request
about: Use this template for pull requests.
title: "[ISSUE NO.]: [SUMMARY OF CHANGES]"
labels: 
assignees: solomonhawk
---
## Related Issues

## Summary of Changes

## Steps to Test

## Screenshots / Video

Note: For more fine-grained control, GitHub supports creating multiple PR templates and choosing which one to use when opening a new PR by specifying a query parameter.

.github/ISSUE_TEMPLATE/feature_request.md:

Issue templates are similarly useful for ensuring consistent, high quality reporting of problems and new feature requests.

---
name: Feature Request
about: Suggest an enhancement for the app.
title: "[Feature Request]: [FEATURE NAME]"
labels: tracking issue, needs triage
assignees: solomonhawk
---
Please provide a description and any relevant details of the feature request below. Once you're done, it will be automatically assigned to the project manager for review.

## Examples
If applicable, add any existing examples or screenshots about the feature request here.

.github/ISSUE_TEMPLATE/bug_report.md:

---
name: Bug Report
about: Use this template for reporting bugs.
title: "[Bug Report]: [PROBLEM SUMMARY]"
labels: bug, needs triage
assignees: solomonhawk
---
Provide a brief summary and any relevant details of the bug below. Once you're done, it will be automatically assigned to the project manager for triage.

## Expected Behavior

## Actual Behavior

## Steps to Reproduce
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

## System & Environment Details
**Desktop (please complete the following information):**
 - OS: [e.g. iOS]
 - Browser [e.g. chrome, safari]
 - Version [e.g. 22]

**Smartphone (please complete the following information):**
 - Device: [e.g. iPhone6]
 - OS: [e.g. iOS8.1]
 - Browser [e.g. stock browser, safari]
 - Version [e.g. 22]

## Additional Context
Add any other context about the problem here.

The next article in this series is Maintenance Matters: Default Formatting.

Solomon Hawk

Solomon is a developer in our Durham, NC, office. He focuses on JavaScript development for clients such as ID.me. He loves distilling complex challenges into simple, elegant solutions.

More articles by Solomon

Related Articles