Index Ai Academy
Phase 11 · Professional practice · Career

Writing for engineers

READMEs, design docs, commit messages and bug reports people can act on.

About 35 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

As you grow as an engineer, more of your impact comes through writing: a design doc that gets a team to agree, a README that lets a stranger run your project in five minutes, a bug report that gets fixed instead of ignored, a commit message that explains a strange line two years later. Clear writing is a skill like coding, with patterns you can learn. AI tools write a lot of text now too, which makes knowing what good looks like more valuable, not less.

What you'll be able to do

  • Write for a specific reader: lead with the point, use short sentences and concrete examples
  • Structure a README, a design doc and an architecture decision record
  • Write commit messages and bug reports that someone can act on months later
  • Build checkers for commit messages and bug reports

What it covers

  1. Principles
  2. Commit messages
  3. Checking a subject line
  4. Bug reports
  5. Does this report have what it needs?
  6. Comments and docstrings
  7. Imperative or not?
  8. Don't repeat the diff

Principles

A road sign is short, plain and placed where you need it, because the reader is moving fast. Engineers read the same way.

  • Know the reader. A new user, a reviewer and an on-call engineer at 3 a.m. need different things.
  • Lead with the point. Put the conclusion or the request first, then the detail. Busy readers stop early.
  • Be concrete. "Checkout p95 latency rose from 300 ms to 2.1 s after deploy 4812" beats "checkout is slow".
  • Short sentences, plain words. Define terms the reader may not know.
  • Structure it. Headings, short paragraphs, lists, tables, code blocks.
  • Cut. Remove what the reader doesn't need. Then cut again.

The README

The front door of a project. In order:

  1. What it is, in one or two sentences, and who it's for.
  2. Quick start: the exact commands to install and run it.
  3. A short usage example.
  4. Configuration: the settings and environment variables.
  5. How to run the tests and contribute.
  6. License.

Test it: can someone new get it running using only the README?

Design docs and decision records

Moving a wall on a drawing costs an eraser. Moving it in a finished house costs a fortune. Before building something big, write a design doc: context and the problem; goals and non-goals (what you're deliberately not doing); the proposal; alternatives considered and why not; risks; rollout plan; open questions. It's cheaper to change a document than a system, and reviewers can catch problems early.

An architecture decision record (ADR) is a short note for each significant decision: the context, the decision, and its consequences. A folder of ADRs tells future engineers why things are the way they are.

Quick check
A design doc for a new search feature lists "Non-goals: search inside PDFs". Why include it?
  1. To make the doc longer
  2. Non-goals are required by law
  3. Because PDFs can't be searched
  4. It sets clear limits, so reviewers don't expect it and scope doesn't creep
Show the answer

It sets clear limits, so reviewers don't expect it and scope doesn't creep

Saying what you won't do is as useful as saying what you will.

Commit messages

Picture a note taped inside a wall for whoever renovates next. A commit message is a note to the next person who runs git blame on this line, often you.

Retry webhook delivery on 5xx responses

Partners' endpoints sometimes return 503 during their deploys, and we
dropped those events. Retry up to 5 times with exponential backoff, and
send failures to the dead-letter queue so support can replay them.

Fixes #412

The conventions:

  • A subject of about 50 characters, 72 at most, in the imperative mood, the form you'd use for a command: "Add", "Fix", "Retry", as if completing "This commit will...". No period. One that follows all of this: "Retry webhook delivery on 5xx responses".
  • A blank line, then a body that explains why and any trade-offs. The diff already shows what.
  • Wrap the body at 72 characters.
  • Some teams use Conventional Commits prefixes, such as fix: and feat:, for automated changelogs.

"fix", "update" and "wip" tell nobody anything.

In plain words: the diff shows what changed. Your message says why.

Worked example

Checking a subject line

Several of the conventions in this section can be checked by a tool, so nobody needs to argue about them in review.

def check_subject(subject):
    problems = []
    if len(subject) > 72:
        problems.append("over 72 characters")
    if subject.endswith("."):
        problems.append("ends with a period")
    if subject[:1].islower():
        problems.append("starts with a lowercase letter")
    if subject.lower() in {"fix", "update", "wip", "changes"}:
        problems.append("says nothing")
    return problems or "ok"

for s in ["Retry webhook delivery on 5xx responses", "fix", "updated the thing.",
          "Add retry logic to the webhook delivery client so failed events are not lost for partners"]:
    print(repr(s[:44]), check_subject(s))
It runs right here in your browser, with nothing to install.

A tool can catch the form, but only a person can tell you whether the message explains why. Automate the first, so that reviewers can spend their attention on the second.

Try it now

The diff shows what changed. The message is for why.

Bug reports

A doctor can't do much with "it hurts". Where, since when, and what makes it worse? A good bug report gets fixed. It has:

  • a specific title: "Checkout total ignores discount when cart has 10+ items";
  • steps to reproduce, numbered, starting from a clean state;
  • expected and actual results;
  • the environment: version, browser or OS, account type;
  • logs, screenshots, and a minimal reproduction if you can make one.
Worked example

Does this report have what it needs?

Most slow bug fixes start with a report that's missing something. Here are two reports, checked for the parts this section lists.

NEEDS = {"steps": ["steps", "1."], "expected": ["expected"], "actual": ["actual"], "version": ["version", "v1.", "v2."]}

def missing(report):
    text = report.lower()
    return [part for part, words in NEEDS.items() if not any(w in text for w in words)]

print(missing("Checkout is broken, please fix ASAP!"))
print(missing("""Total ignores discount with 10+ items (v2.4, Chrome)
Steps: 1. Add 10 items 2. Apply SAVE10
Expected: $90. Actual: $100."""))
In the lesson you can change this and run it.

The first report needs several rounds of questions before anyone can start. The second can be picked up right away. Issue templates are this idea in a form: they ask for those parts before the report gets sent.

Comments and docstrings

Code says what; comments should say why. A note on a recipe says "go easy on the salt, the stock is salty already", not "add salt". So write the reason for an odd choice, a link to the bug it works around, a warning. Docstrings describe a function's contract: what it takes, returns and raises.

Quick check
Which bug report title helps most?
  1. Checkout broken
  2. Something is wrong with prices
  3. Checkout total ignores discount when the cart has 10+ items
  4. URGENT BUG!!!
Show the answer

Checkout total ignores discount when the cart has 10+ items

A specific title says where, what and when. It's half of a good reproduction.

Worked example

Imperative or not?

A simple check on the first word of a subject catches most past-tense subjects.

def first_word(subject):
    if ":" in subject.split()[0]:
        subject = subject.split(":", 1)[1].strip()
    return subject.split()[0].lower()

for s in ["Added retries to webhooks", "fix: Handle empty carts", "Fixing the tests", "Bring back dark mode"]:
    w = first_word(s)
    print(f"{w:10}", "looks past/continuous" if w.endswith(("ed", "ing")) else "fine")
In the lesson you can change this and run it.

"Bring" ends in -ing but is imperative, so a real checker needs a list of exceptions. Heuristics need care.

Quick check
Checking a commit subject. What does this print?
subject = "Fix: retry webhook delivery on 5xx responses."
print(len(subject) <= 50, subject.endswith("."))
  1. False True
  2. True True
  3. False False
  4. True False
Show the answer

True True

It's 45 characters, so the first test gives True, and it ends with a period, so the second gives True too. It's short enough, but subjects shouldn't end with a period.

Don't repeat the diff

A commit message saying "changed timeout from 30 to 60" tells the reader nothing the diff doesn't. Someone finding this line in two years wants to know why. "Partner API takes up to 45 s at peak, so 30 s caused failed syncs" answers that. It also tells them when the change can safely be undone: when that partner gets faster.

Lead with the conclusion

Most technical writers build up to their point, the way we were taught in school: background, then analysis, then conclusion. Busy readers stop partway, so they miss the only part they needed. Turn it around, the way a news story does. Start with what you want or what you found. Then give the reasoning for whoever wants it.

For example, begin with "I recommend Postgres over DynamoDB, mainly because our queries need joins." The reader knows your answer straight away, and they can decide how much of the rest to read.

Why non-goals earn their place

Writing down what you're deliberately not doing feels like wasted space, and it may be the most useful part of a design doc. It stops people from asking "shouldn't this also handle X?" in every review, and it keeps the project from quietly growing. If a reviewer strongly disagrees with a non-goal, you've found that out before building anything, which is exactly when you want to.

Comments should explain why

A comment that says what the code does goes stale, and it adds nothing. # add 1 to retries above retries += 1 is only noise. Save comments for what the code can't say: "The vendor rejects batches over 500, even though their docs say 1,000."

Practice what you just read

  • 3 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 5 review cards for spaced repetition

Create a free account

Try a quiz question

Which commit subject follows the conventions?

  1. Retry webhook delivery on 5xx responses
  2. Retried webhooks.
  3. fixed stuff
  4. This commit changes how the webhook sender behaves when partner endpoints return server errors

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson