Write commit messages for future you
I've spent countless hours digging through git logs trying to understand why a particular line of code exists, only to find a commit message like "fix stuff" or "update". Every time, I wish I could talk to the person who wrote it. Six months from now, that person will be you, and you won't remember the context. A good commit message explains why, not just what, turning your git history into a living document that helps future maintainers—including yourself—understand the decisions behind the code.
The imperative subject: under 72 characters, present tense
Your commit subject is the first and often only line someone reads. It needs to be clear, concise, and written in the imperative mood—as if you're giving a command to the codebase itself. Think of it as completing this sentence: "If applied, this commit will [your subject]."
The 72-character limit forces you to be specific. It's not arbitrary; it comes from email best practices and ensures your message doesn't wrap awkwardly in terminals or GitHub diffs. When you hit that limit, you're forced to drop the fluff and get to the point.
Here's the difference:
- Bad: "Fixed the payment webhook thing" (35 chars, but vague)
- Good: "fix(payments): retry webhook on gateway timeout" (48 chars, clear)
The second one tells you what changed and why in one glance. You can scan a week's worth of commits and understand the shape of the work. When you run git log --oneline, you're not squinting at cryptic abbreviations.
The body: explain the reason and trade-offs
This is where most developers stop thinking. They write a subject and call it done. The body is where you write for future you.
The body should answer three questions:
- What problem does this solve?
- Why did we solve it this way and not another?
- What trade-offs did we accept?
Look at this example:
fix(payments): retry webhook on gateway timeout
The gateway returns 504 under load. Retry with
backoff so orders don't stay pending.
Refs: PAY-214This tells you: the problem is 504 errors under load, the solution is retry logic with backoff, and it's tied to a ticket. Someone reading this six months later knows there's a specific failure mode this code addresses. If they're optimizing payment flow, they won't rip out the retry logic thinking it's defensive paranoia.
Compare that to a common alternative approach:
fix(payments): retry webhook on gateway timeout
Added exponential backoff to webhook retry logic.
Max 3 retries, 1s initial delay. Used jitter to
prevent thundering herd on recovery.
Trade-off: delayed order confirmation in edge case
but ensures no orders get stuck in pending state.
Refs: PAY-214This version is even better. It explains the algorithm choice (exponential backoff with jitter), acknowledges the trade-off (delayed confirmation), and justifies why it's worth it (prevents stuck orders). When someone later asks "why not queue this to a background job," the body already has that conversation.
In real projects, I've seen developers iterate on payment retry logic multiple times because nobody documented why the original approach was chosen. The third developer added circuit breakers. The fifth developer switched to a dead-letter queue. Each person made a reasonable decision in isolation, but the whole history is thrashing because nobody explained the constraints.
Reference the ticket: context beyond the code
A commit message lives in git. A ticket lives in Jira or Linear or whatever your team uses. They're two separate systems, and you need to bridge them.
When you write Refs: PAY-214 or Closes #1842, you're creating a link for future readers. Someone debugging in production can see the commit, look up the ticket, and find the original problem statement, discussions, and decisions that led to the code.
The commit message and the ticket serve different purposes:
- Ticket: Why did we decide to do this? What's the business context? Who approved it?
- Commit: How did we implement it? What are the technical constraints?
A ticket might say "Customers in India are experiencing order failures during peak hours." The commit message says "Added exponential backoff to retry gateway 504s." Together, they're complete context.
Many platforms support automatic linking. GitHub closes issues when you use Closes #123. Jira auto-links commits with PAY-214. Use these features; they're not extra work, they're just a tiny discipline that pays off when someone's debugging production at 2 AM.
Conventional commits: automated changelogs and clarity
Conventional Commits is a simple specification that standardizes your commit messages. It looks like this:
type(scope): subject
body
footerThe types are standardized: feat for features, fix for bugs, docs for documentation, refactor for code changes that don't add features or fix bugs, perf for performance improvements, test for tests, ci for CI/CD changes, and chore for maintenance.
Why does this matter? Because tools can parse it. With Conventional Commits, you can:
- Auto-generate changelogs by filtering
featandfixcommits - Automatically bump semantic versions (a
featis a minor bump, afixis a patch) - Run automated linting on commit messages to prevent typos like
fic(payments) - Filter logs to see only feature work or only bug fixes
I set up commitlint and husky on every project now. If a developer tries to commit with a message that doesn't follow the convention, the hook rejects it locally. No noise in the git history, no debates about style.
A real changelog generated from Conventional Commits looks like this:
| Version 1.2.0 |
| Features: - Add retry logic for payment gateway timeouts (PAY-214) - Support webhook signature verification with RS256 Bug Fixes: - Fix webhook body encoding for non-ASCII characters - Prevent duplicate order notifications |
Your users know what changed. Your support team knows what to tell customers. Your release notes write themselves. This saves hours of manual work per release.
Common mistakes
Mistake 1: Vague subjects. "Update code" or "Fix issue" tells nobody anything. Spend ten seconds on specificity. "fix(auth): validate JWT expiry before cache lookup" is clear enough to act on without reading the body.
Mistake 2: Repeating the diff in the message. Your commit message shouldn't explain what changed syntactically; the diff does that. Explain why it changed. "Add null check on user ID" is obvious from the code. "Add null check on user ID to prevent crash when webhook arrives before user creation completes" explains the real issue.
Mistake 3: Omitting trade-offs. Every decision has trade-offs. Mentioning them prevents future developers from undoing your work. "We prioritize availability over consistency here" or "This query is slow but avoids an N+1 problem" saves someone hours of confusion later.
Mistake 4: Forgetting the ticket reference. I've seen teams where commits and tickets drift apart. Six months later, the codebase has the change but nobody knows why. Always link back.
Mistake 5: Mixing concerns in one commit. A commit should be atomic—one logical change. If your subject needs the word "and" in it, you probably need two commits. "fix(payments): retry logic AND add logging" should be two commits. This makes reviewing easier, reverting easier, and blame-tracking easier.
Key takeaways
- Write commit subjects in imperative mood, under 72 characters, explaining what the commit does
- Use the commit body to explain why, not what—the diff shows what changed
- Always reference the ticket that drove the change, creating a link between code and context
- Adopt Conventional Commits to enable automation: auto-generated changelogs, semantic versioning, and commit linting
- Document trade-offs so future developers understand why you chose this solution over alternatives
- Keep commits atomic—one logical change per commit, even if it means multiple small commits
- Think of your commit message as documentation for future you, written at the moment you understand the problem best






Comments
Loading comments…