Writing Good Documentation

Writing good documentation is not easy, but very often, it’s not easy because taking the first step is hard. Instead of beating myself over how I’ll document a new project every single time, I created some guidelines for myself to make that process less tedious. It doesn’t get any more meta than this: documenting documentation.

I’ve seen my fair share of documentation, some good and some bad, and this is the conclusion I drew as well as the rules I follow to document my projects.

There are two types of repositories: those that run, and those that don’t.

While the code for both of these should be self-documenting (i.e. aptly naming your functions and variables) and well documented (i.e. through the typical comments above each function, classes, etc.), good documentation require documentation outside the code, such as in your project’s README file or in a separate website.

This is why I split the types of repositories in two; while both are ultimately code, the documentation itself is targeting different audiences.

Those that run - applications and such - can target anybody, from a programmer, to a system administrator with very little experience writing code, or even your average Joe that just wants to try your application on his brand new NAS which he just found out has support for a thing called “containers”. This means that they want to know how to use your application, not the code.

Conversely, those that don’t run - like libraries and frameworks - target programmers. In other words, unlike the previous type, they want to know how to use the code.

There are still things that both types of repositories should have, like a brief description of the purpose of the project.

Something as simple as the following would do just fine for a summary: > potato-peeler is a Java library for peeling potatoes asynchronously with support for customizable > blade sharpness, peeler durability and rate limiting.

Before we shift our focus to the type-based approaches, keep in mind that these suggestions target documentation at the repository level, not the documentation at the scale of a fleet of microservices. If you’re looking for help with documenting an entire system containing dozens of applications and their relationship with each other, that’ll be covered in a different article at some point. That doesn’t mean each of your individual microservice shouldn’t be documented. Good documentation is about documenting components with the appropriate level of detail. The documentation present in each of your individual repositories should be specific enough to cover the application itself, while the documentation one level higher (i.e. the documentation of the system/architecture as a whole) should focus on the interactions and relationships between each application rather than the specificities within the individual applications themselves.

Those that run

Out of the box, you should offer instructions for your user to start your application locally, no matter how complex it is. This can be done through several ways: - Step-by-step guide (command(s) to start the application, e.g. docker run -p 8080:8080 --name gatus twinproduction/gatus) - Docker Compose file (Docker) - Helm chart or YAML files (Kubernetes) - Makefile - All of the above

This isn’t just important for the sake of documentation, this is important if you want your projects to gain traction. If your users can’t easily start using your project, they might just move on to the next option available without even giving it a try.

With this out of the way, a quick overview of the features that your application has to offer would be a great addition. If said application is part of a set of microservices and its purpose isn’t necessarily to add features to the platform as a whole, but rather handle one or multiple responsibilities, it would be preferable to include a summary of the role(s) of the application as well as its responsibilities.

Assuming that your application hooked the user’s interest and they had a feel for it, you need to move into the details.

Every single configuration parameter, be they environment variables, parameters in a YAML file, command line flags or arguments must be documented, even if the parameters seem obvious to you.

There are many ways of documenting this, but my preference lies with a simple table: | Environment variable | Description | Default value | |:———————|:———————————————|:————–| | DB_USERNAME | Username to use to connect to the database | root | | DB_PASSWORD | Password to use for the given DB_USERNAME | hunter2 | | DB_HOST | Database host (e.g. example.com:5342) | "" | | WEB_PORT | Port on which the web application is exposed | 8080 |

Documenting each configuration parameter is non-negotiable, especially for open source stand-alone projects, since the overwhelming majority of the users that will look at your application will not look at the code a single time. If you’re adding a feature for your users that can only be enabled with a specific configuration, you have to document that feature, or they will most likely never even know that the feature exists.

Finally, if your application offers features that requires a complex configuration to enable (i.e. several parameters, flags or environment variables), you should add a section with an example. Not only will this provide an opportunity to showcase said feature - bonus points if you include a screenshot, assuming it makes sense to include one for the given feature - under the premise of documentation, but it will also possibly save you from people asking you how to use the feature, or even worse, not using the feature at all because they couldn’t get it to work.

Those that don’t

Unlike the previous type, libraries are not executed by just any end user, they’re programmatically called by a developer. This means that you need to document the API of your library, namely, the core functions, but let’s take it one step at a time.

Much like applications, you do need to specify how to get started with your library, the only difference being that your focus is on the code this time, and depending on the language, the process to add a library.

After your brief description, the first thing you want to have is the instructions for installing library followed by the minimum code required to use the library.

For a Java library, it could be as simple as this:

Installation

To use this library with Maven, add the following to your dependencies:

<dependency>
  <groupId>org.twinnation</groupId>
  <artifactId>potato-peeler</artifactId>
  <version>1.0.0</version>
</dependency>

Usage

You can then create a PotatoPeeler like this:

PotatoPeeler peeler = new PotatoPeeler();
Potato potato = new Potato();
peeler.peel(potato);

Or for a Go library:

Installation

go get -u github.com/TwiN/g8

Usage

Just want a simple layer of security without the need for advanced permissions? This configuration is what you’re looking for:

gate := g8.NewGate(g8.NewAuthorizationService().WithToken("mytoken"))
router := http.NewServeMux()
router.Handle("/unprotected", yourHandler)
router.Handle("/protected", gate.Protect(yourHandler))
http.ListenAndServe(":8080", router)

Now that we have the installation steps as well as an example with the minimum code required to use the library out of the way, the next aspect of your library you’ll need to document is the exported functions themselves. While documenting them in your code is good, I personally believe it’s insufficient, because most of your potential users will first look at the repository’s README to determine whether your library has what they require or not. If the one feature they’re looking for isn’t showcased through an example or though a documented function, that means you risk losing that one potential user.

This can be documented in many ways, one of my favorite ways is simply using a table, though sometimes I will mix using a table and showing examples.

Function Description
withSharpness Set a specific level of sharpness for the potato peeler. The higher the sharpness, the faster the peeler peels potatoes.
withConcurrency Set the maximum number of potatoes that a peeler can peel at the same time.
withDurability Set the durability of the potato peeler. The higher the durability, the longer it lasts before needing to be replaced.

If your library has several distinct features, I’d also suggest adding a section for each of these distinct features with an example of how to use said feature. Assuming you’ve been following the same order I’ve suggested so far, we’ve already documented the individual components of your library, meaning that what’s left is to document the intrinsic relationship between these components and the features they can be leveraged through.

To wrap it up, I occasionally add an FAQ section at the end of my documentation where I add common problems/questions that people encounter, sometimes even taking the initiative to add a question and its answer before anybody asks it if I think that a certain feature might benefit from some clarification.