DEV Community

RAXXO Studios
RAXXO Studios

Posted on Originally published at raxxo.shop

Writing Commit Messages That Double as Documentation

  • A three-part commit structure (what, why, impact) answers 90% of git blame questions

  • Bad messages like "fix bug" cost me 20 minutes per investigation months later

  • The body matters more than the subject line for future archaeology

  • Templates and a commit hook make the good format the default

Six months after I shipped a feature, I ran git blame on a weird line of code and found a commit that said "fix stuff". That commit was mine. I had no memory of what stuff, or why. The fix was to write messages that answer the questions I'd actually ask later.

A commit message is the only documentation that sits right next to the code it describes. Most people waste it.

The Three Questions Future You Actually Asks

When I run git blame on a confusing line, I'm never asking "what does this code do". I can read the code. I'm asking why it exists.

The three questions are always the same: what changed, why it changed, and what it affected. If a commit message answers those three, I close the terminal and get back to work. If it doesn't, I go spelunking through pull requests and chat logs that may not exist anymore.

Here's a real bad one from my own history: update handler. That's the whole message. When I found it later, I had to read the diff to learn it added a timeout to an API call. The diff told me the what. It couldn't tell me the why. Was the timeout a guess? A response to a real hang? Did a specific endpoint need it? I had no idea, so I was scared to touch it.

Compare that to a message that says the timeout was added because the upstream payment API would hang for 30 seconds under load, and a 5 second timeout with a retry was faster for the user. Now I know the number isn't random. I know what breaks if I remove it. I can change it with confidence.

The "why" is the part that dies first.

Code carries its own "what". A commit carries the "what" for a day, then the diff carries it forever. But the reasoning in your head evaporates fast. You'll forget the production incident, the slow endpoint, the edge case a customer hit. Write that down while it's fresh, because the code physically cannot store it.

The impact line is the one people skip most and regret most. "This also changes the shape of the cache key, so old cached entries become stale on deploy." That sentence saves someone a confusing afternoon. I've written versions of it and been grateful to past me more times than I can count.

So the method is simple: answer those three questions in order, every time. That's all it takes.

The Structure I Actually Type

A good commit has a subject line and a body, separated by a blank line. Git treats that first line specially, so it carries weight.

The subject is 50 characters or fewer, written as a command: "Add timeout to payment handler", not "Added" or "Adding". Present tense, like you're telling the codebase what to do. It should finish the sentence "if applied, this commit will..."

Then a blank line. Then the body, wrapped around 72 characters, where the real work happens.

My body has three parts, and I don't label them. I just write paragraphs in this order:

First, what changed in plain language. Not a restatement of the diff, a summary a human can read. "The payment handler now times out after 5 seconds and retries once."

Second, why. This is the paragraph that justifies the change existing. "Under load the upstream API hangs for up to 30 seconds. Users saw a frozen checkout. A short timeout with one retry fails faster and recovers in most cases."

Third, impact and anything surprising. "Side effect: the retry doubles the request count on genuine slow responses. If the upstream recovers, this is fine. If it's truly down, the second request also fails fast."

That's it. Most of my commits are four to six lines of body. Some are one line because the change is trivial and self-explanatory ("Fix typo in error string"). The structure scales down.

The retry example above isn't hypothetical in spirit. Getting retries right is its own topic, and I covered the pattern I reuse in Retry With Backoff and Jitter. A commit that adds a retry should point at the reasoning behind the numbers, even if only in one sentence.

One thing I avoid: dumping ticket IDs as the entire message. "JIRA-4471" tells future me nothing without a login and a working link. Reference the ticket at the bottom if you must, but the message has to stand alone. Issue trackers get migrated, archived, and shut down. The git history outlives all of them.

Before and After on Real Commits

Let me show the difference with the kind of change I make all the time.

Before: migrate db. After: a subject of "Add nullable discount_code to orders" with a body explaining that the column is nullable so the migration runs without a default backfill on the existing 80,000 rows, and that application code treats null as "no code applied".

The before version is a landmine. Six months later, someone wonders why the column is nullable instead of defaulting to an empty string. The after version answers it in advance. Schema decisions are exactly the kind of thing you forget and then second-guess. If you want the deeper context on making schema changes safe, see Database Migrations I Can Roll Back at 2am.

Another one. Before: perf. After: "Switch catalog lookup to binary search" with a body noting the list is pre-sorted and has 50,000 rows, and that linear scan was showing up in profiling. Now anyone reading it knows the precondition (the list must stay sorted) and won't accidentally break it. I measured that exact tradeoff in Binary Search vs Array.includes(), and a one-line reference in the commit saves a re-benchmark.

A third. Before: fix logging. That's useless because logging changes are often subtle and load-bearing. After: "Add request ID to structured log context" with a body saying every log line in a request now carries the same ID so I can grep a full trace, and that this depends on the middleware running first.

See the pattern? The before messages are all two words and all worthless. The after messages take maybe 40 extra seconds to write and answer the question before it's asked.

I keep logs structured partly so these commit messages have something concrete to reference. The two habits feed each other, which I get into in Structured Logging I Add on Day One.

The 40 seconds feels like a tax when you're typing it. It's not. It's a loan to future you at a great interest rate. One avoided 20 minute investigation pays for 30 careful commits. I've been on both sides of that trade enough to know which I prefer.

Making the Good Format the Default

Discipline fails. I don't trust myself to write a good message every time on willpower, so I made the environment push me toward it.

The first thing is a commit template. Git lets you set one with a config pointing at a text file. Mine has comment lines reminding me of the what, why, impact order. When I run git commit without -m, my editor opens with those prompts already there. I delete the comments as I fill them in. The friction is gone because the structure is sitting in front of me.

The second thing is banning -m for anything non-trivial. The -m flag is how bad messages happen, because you're typing into a cramped shell with no room to think. Open the editor. Give yourself space. Typos in a one-line flag become "fix stuff" commits.

The third thing is squashing before I merge. My work-in-progress commits say "wip" and "oops forgot import" and I don't care, because they never reach the main branch. I squash the whole branch into one clean commit with one good message that describes the finished change. The history people read should be the story of what shipped, not the mess of how I got there.

I also use AI to draft the first pass now. I paste the diff and ask for a what/why/impact message, then I fix the "why" because the model can only guess at my reasoning. It gets the "what" right almost every time, which is the boring part anyway. If you want the setup I use for that kind of task, the Claude Blueprint walks through how I wire these helpers in.

A warning on automation: don't let a tool write the "why" for you and ship it unread. The model infers intent from the diff, and sometimes it's wrong in a way that sounds confident. A wrong "why" in the permanent history is worse than a blank one. Read what it drafts.

If you're on a solo project and think none of this matters because you're the only reader, you're the exact person it helps most. There's no teammate to ask. The git history is your only record. Past me is a stranger I can only talk to through commit messages.

Bottom Line

Start with one change today: stop using git commit -m for anything bigger than a typo. Open the editor, write three short paragraphs answering what, why, and impact, and see how it feels in a month when git blame lands on it.

If you want to go further, set a commit template so the three prompts greet you every time. That single config line did more for my history than any amount of trying to remember.

The payoff isn't visible on the day you write the message. It shows up later, when a confusing line has a clear explanation attached and you close the terminal in ten seconds instead of digging for twenty minutes. Good commits are a message to a person who's forgotten everything. That person is you.

Top comments (0)