Agreements are an essential part of software development. They lower development costs and make developers' lives easier. But there is a problem - they often complicate things because they aren't properly documented and conveyed across the team by word of mouth, like old fairy tales. While spreading, agreements change. Suddenly, new details appear, and old ones are gone. By the end, every team member has their own agreement picture in the head, and even that picture sometimes fades away.
Even worse - when teams start documenting these agreements, they do it haphazardly and often create a mess of loosely coupled documents, half of which are not even up-to-date.
In this article, I'll tell you the right way of documenting agreements, so they will help you.
So, how can we make agreements helpful? We not only have to document them but also do it so that:
they were easy to use;
following these agreements required minimal effort;
it would be easy to understand if these agreements are still valid;
it would be easy to understand why these agreements even exist;
ideally - they were automated.
One can come up with lots of ways to classify agreements. I will split them by their abstraction level:
Agreements on different levels require different ways to document them and bring different benefits. Let's take a look at each level.
The aim of these agreements is to make code uniform, comprehensive, and readable. Here are some examples:
We use double quotes instead of single ones.
We don't call ENV directly from the code except in the Config
class, where we wrap these calls into methods.
Service objects have postfix Service
and one public method call
.
These types of agreements are created to lower the reader's cognitive load and help him become used to unknown code faster. Like Martin said, code is read up to 10 times more than written.
Despite your opinion on the Ruby on Rails framework - it has an incredible convention over configuration
principle at its core, which allows any Rails developer to open someone else's project and immediately navigate it pretty well.
So how to document these conventions? Linter tool! If there is no suitable linter rule, write your own lint. Almost every linter allows you to do that: here is an example in Go language, and here is for Ruby.
Using linter for such conventions brings you three benefits:
There is no need for a developer to think about them - linter will highlight every error and often even fix them for you.
If you use code reviews in your team - you free your reviewers from thinking about these things and give them more time to look at more important things.
The developer will see a problem at the very start of the development cycle, so he will fix it immediately without spending time to return to the context later. It becomes cheaper to keep the agreement.
One more bonus: writing a new linter rule is excellent training for a junior developer. While completing this task, he will learn a lot about code parsing and building AST and understand language more deeply.
This is a higher-level type of agreement that aims to make your architecture thoughtful, coherent, and uniform. A few examples:
We use the Python to write regular services and Elixir in the highloaded parts of the system.
The backend returns errors in described format.
Each service is required to send metrics in prometheus, on the /metrics
endpoint, the port for sending metrics is configured by the PROMETHEUS_PORT
environment variable.
Such agreements not only reduce cognitive load, but also solve three more problems:
Reduce operating costs. If the services are launched in the same way, with same logs format, publishing the same metrics, then it is much easier to maintain the service and cope with incidents.
Reduce design costs. The developer doesn't need to design the architecture from scratch every time - you thought in advance, and now he needs to design only a specific feature or service, without worrying about basic things.
Reduce communication costs. If the server's response or event format in Kafka is predetermined, developers don't need to discuss their interaction each time, instead they can simply refer to the convention.
Such agreements are more complex, and I prefer to fix them in two steps.
Step 1 - Describe
Architecture Decision Record (ADR) is a tool for documenting such agreements. Its charm is that it captures meta information along with an agreement: why such an agreement was adopted; what alternatives were discussed; when it was last revised; is the agreement still valid?
This allows the new team member to understand the reasons for the decisions made and not ask people around about it.
ADR consists of several main blocks:
What problem does the agreement solve?
What options for solving the problem were considered, and what were their pros and cons?
Which option was chosen in the end?
There may be additional blocks - for example, implementation costs calculation.
It's more convenient to keep ADR in a system where one can see the history of changes and discussions. My choice is Github and Notion, each with its pros and cons. The advantage of Github is that it has a review tool and version history out of the box. Notion can be a good solution due to the convenience of working with databases and tags. And also - non-developers can easily handle it.
If you want to get started with ADR, I recommend looking at the repository, where you can find different ADR templates and examples of how to use them.
Step 2 - Automate
ADRs are more challenging to automate than code-level conventions: design linters have yet to be invented (what a pity!). Nevertheless, it is possible to partially automate them, depending on what kind of agreement it is.
Create and update service templates for agreements on languages, libraries, and embedding services into infrastructure. Then the developer won't write new services from scratch but rather copy it from the template and immediately receive the configured Dockerfile, metrics publishing, etc.
Similarly, you can create class generators within one application. Suppose you had agreed on several application layers (controller => form => service object). In that case, you can make a simple console command that will generate all layers for a new feature at once.
If you have agreed on some principles that cannot be automated in this way, you can organize checklists that are automatically added to a merge request or a task in the tracker; thus, the developer can quickly go through them before passing the task on.
There are many agreements for processes in each company, for example:
Description of how hiring in the company works.
Description of the release rollout process.
Requirement for design reviews for large tasks.
Conducting a team meeting twice a week with a discussion of current tasks and obstacles.
Until recently, I did not think about documenting these agreements, although they significantly affect the company's success. Documentation of these agreements not only carries benefits of the types mentioned above but also allows you to rationalize the processes, transfer them to the visible plane and think about their expediency.
I got the idea from . He proposed a tool similar to ADR - Process Decision Record (PDR). The only difference is that instead of architectural decisions, it describes decisions about processes. In addition, he suggested putting a "rethink date" in each PDR - a date when you return to the document to see if it still solves your problems in the best way, n months after adoption (by the way, the same can be done with ADRs ).
As for automation, there is little you can do. You can automate some processes by setting up a workflow in Jira, setting reminders for meetings, or creating a bot that automatically prepares a presentation of the week's results (I did this, though, in a foreign company).
But often, you can't really automate processes, and your main goal is to make them easier to follow than not follow. Nevertheless, documenting agreements will still be helpful, even if your processes are already easy to follow - formalization and rationalization will allow you to improve them.
Documentation and subsequent automation are beneficial: time spent on development decreases, applications become more supportable, and processes become smarter.
One could think that all this is unnecessary bureaucracy because "we are good guys - we can develop code without it." But in fact, agreements will save you substantial amounts of time and money and protect employees' nervous cells. Sure, you don't need to deal in absolute and reject anything that goes against agreements - this may be a sign that the agreement needs to be updated or that you didn't initially think about some of its aspects.
If you haven't yet started documenting agreements in your team, move from lower abstraction levels to higher ones: start from code-level agreements, then architecture-level, and only then cope with the processes-level. Agreements documentation is a habit to develop in your team, and starting with less abstract concepts is much easier.
In addition, not all types are described in the article. For example, you can document the design agreement as library components.
Each new type of agreement goes through the same three stages:
Figure out how to document it.
Figure out how to automate it.
As time passes, ensure it saves more resources than it takes to keep it.
And the last one. During the documentation process, it may be found that some of the existing agreements are apparently, not justified by anything, waste your team's time, and are generally harmful, but you are already used to them. In this case, you have to overcome the habit barrier in the team's minds and convince your team that the rejection of agreements is sometimes more important than accepting them. But that's an entirely different story.
Also published here.