Mental Model
Your Git history is the only documentation that is guaranteed to be up-to-date. Architecture docs go stale, wikis get abandoned, Confluence pages rot. Every commit you make is a permanent, searchable record of a decision your future team will need to understand.
When a P0 incident hits at 3:00 AM and you need to find when a specific behavior changed, you are at the mercy of whoever wrote the commit messages three months ago. If it was you, and you wrote "fix bug" — you are on your own. If you wrote a structured commit with context and reasoning, the diagnosis takes seconds instead of hours.
This lesson teaches you to write commit messages that function as engineering documentation.
The Anatomy of a Professional Commit Message
A commit message has two parts: a subject line (mandatory) and a body (optional but often critical):
<type>(<scope>): <short summary in present tense, max 72 chars>
<blank line>
<body: explain WHY, not WHAT. The diff already shows WHAT.>
<Include: motivation, context, alternatives considered, trade-offs.>
<Wrap lines at 72 characters.>
<blank line>
<footer: Breaking changes, issue references, co-authors>
The Subject Line: 72-Character Rule
Most Git log displays truncate lines longer than 72 characters. The subject must be:
- Written in the imperative mood ("add feature" not "added feature" or "adds feature")
- 72 characters maximum — GitHub, GitLab, and most Git UIs enforce this for display
- A complete thought that stands alone (it will appear in
git log --oneline)
Bad subject lines:
fix
WIP
update code
asdfgh
Fixed the thing that John mentioned in Slack
Good subject lines:
fix(auth): prevent session token reuse after password reset
feat(checkout): add Apple Pay as payment method
refactor(UserService): extract email validation to EmailValidator class
perf(query): add composite index on (user_id, created_at) for order history
The Conventional Commits Standard
Conventional Commits is the industry-standard format for structured commit messages. It enables automated tooling like semantic versioning and changelog generation.
Type prefixes:
| Type | When to use | Versioning impact |
|---|---|---|
feat |
New feature visible to users | Minor version bump |
fix |
Bug fix visible to users | Patch version bump |
refactor |
Code change with no behavior change | None |
perf |
Performance improvement | None |
test |
Adding or fixing tests | None |
docs |
Documentation only | None |
ci |
CI/CD pipeline changes | None |
chore |
Build tools, dependencies, config | None |
style |
Formatting, whitespace (no logic) | None |
BREAKING CHANGE |
Any breaking change | Major version bump |
Scope is optional but valuable — it names the component or module affected:
feat(payment): ... ← payment module
fix(auth): ... ← authentication system
refactor(UserService): .. ← specific class
perf(db): ... ← database layer
The 7 Rules of Great Commit Messages
These rules have been standard engineering practice since Tim Pope's influential post:
- Separate subject from body with a blank line
- Limit the subject line to 72 characters
- Capitalize the subject line
- Do not end the subject line with a period
- Use the imperative mood in the subject line
- Wrap the body at 72 characters
- Use the body to explain what and why, not how (the diff shows how)
Applying Rule 7: What vs. Why
# BAD: Explains WHAT (which the diff already shows)
fix(UserService): changed the email regex from /^\S+@\S+\.\S+$/
to /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ in the
validateEmail method
# GOOD: Explains WHY (what the diff cannot show)
fix(auth): accept RFC 5321-compliant email addresses in signup
The previous regex rejected valid emails containing '+' signs
(e.g., user+tag@example.com). These are valid per RFC 5321 and
common in Gmail aliases used for email filtering.
The new regex follows the pattern recommended by the OWASP Email
Validation Cheat Sheet. We deliberately chose not to use the full
RFC 5322 regex (which handles edge cases like quoted strings) as it
is significantly more complex and our user base doesn't require it.
Fixes: #1847
Reported-by: Sarah Chen <sarah@company.com>
The Body: What to Include
The body is where you earn the trust of future engineers. A complete commit body answers:
1. What motivated this change?
"The checkout flow was silently dropping orders when Stripe returned a network timeout. Users received no error message and assumed their order was placed."
2. What alternatives did you consider?
"We considered adding a retry with exponential backoff at the Stripe call site, but this would mask the underlying connection instability. Instead, we added a circuit breaker (Resilience4j) and a user-visible error state."
3. What are the known trade-offs?
"This adds ~15ms to the average checkout latency (circuit breaker state check). Acceptable given the frequency of Stripe timeouts in our load testing."
4. What should future engineers know before modifying this?
"The
CircuitBreakerConfig.FAILURE_RATE_THRESHOLDis set to 50%. Do not lower this below 30% — at 30%, normal Stripe API variance starts triggering the breaker incorrectly (tested in staging)."
Real Examples from Production Repos
A Feature Commit
feat(api): add cursor-based pagination to GET /orders
Replace offset-based pagination with cursor-based pagination using
created_at + id as the cursor. This resolves two production issues:
1. Page drift: With offset pagination, if an item is inserted on
page 2 while the user is on page 3, every subsequent page shows
the wrong results. Cursors are stable regardless of inserts.
2. Performance: At 10M rows, OFFSET 500000 requires scanning and
discarding 500K rows on every request. Cursor-based pagination
always uses the index efficiently — O(log n) regardless of page.
Migration: The response shape adds a `next_cursor` field. The
`page` and `total_pages` fields are deprecated but still returned
(populated as -1) for backward compatibility with v1 API clients.
We will remove the deprecated fields in API v3 (Q4 2026).
Closes: PLAT-892
Reviewed-by: Raj Patel <raj@company.com>
A Bug Fix with Root Cause
fix(inventory): prevent overselling during concurrent flash sales
Root cause: Two concurrent checkout requests for the same product
both read stock=1, both pass the stock check, and both decrement
to 0. The second checkout creates a negative inventory event.
The fix uses an optimistic locking pattern with a retry loop:
1. Read current stock with a version counter
2. Attempt atomic decrement only if version matches
3. If version mismatch (concurrent update detected), retry up to 3x
4. After 3 retries, return HTTP 409 (stock conflict) to the caller
Performance impact: ~2% overhead on checkout latency (P99: 45ms
→ 46ms) measured in load testing at 500 concurrent users.
Alternative considered: Pessimistic locking (SELECT FOR UPDATE).
Rejected because it serializes all checkout for a popular product,
creating a bottleneck during flash sales — exactly the scenario we
need to handle.
Load test results: 0 oversells in 10,000 concurrent checkout
attempts (was 23/10,000 before this fix).
Fixes: #2341
Configuring Git for Great Commits
Set Up Your Commit Template
# Create a template
cat > ~/.gitmessage << 'EOF'
# <type>(<scope>): <subject> — max 72 chars ─────────────────────────┐
# │ │
# Allowed types: feat|fix|refactor|perf|test|docs|ci|chore|style │
# ─────────────────────────────────────────────────────────────────────┘
# WHY: What motivated this change?
# WHAT CHANGED: Key behavioral changes (not code details — the diff covers that)
# TRADE-OFFS: What are the known downsides or constraints?
# REFS: Issue links, PR links, related commits
# Closes: #
# See-also: #
EOF
# Register it
git config --global commit.template ~/.gitmessage
Enforce Standards with a Commit-Msg Hook
# .git/hooks/commit-msg (or use Husky for team-wide enforcement)
#!/bin/bash
COMMIT_MSG_FILE=$1
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
# Check Conventional Commits format
PATTERN="^(feat|fix|refactor|perf|test|docs|ci|chore|style|revert|BREAKING CHANGE)(\(.+\))?: .{1,72}"
if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
echo "❌ Commit message does not follow Conventional Commits format."
echo " Required: <type>(<scope>): <subject>"
echo " Example: feat(auth): add OAuth2 login with Google"
echo " Types: feat|fix|refactor|perf|test|docs|ci|chore|style"
exit 1
fi
# Check subject line length
SUBJECT=$(head -1 "$COMMIT_MSG_FILE")
if [ ${#SUBJECT} -gt 72 ]; then
echo "❌ Subject line is too long: ${#SUBJECT} chars (max 72)"
exit 1
fi
echo "✅ Commit message format looks good"
Searching History with Good Commit Messages
Good commit messages make git log a powerful debugging tool:
# Find all commits related to the payment system in the last 3 months
git log --since="3 months ago" --grep="payment" --oneline
# Find when a feature was added
git log --all --grep="cursor-based pagination" --oneline
# Find all breaking changes
git log --grep="BREAKING CHANGE" --oneline
# See what changed in a specific component over a quarter
git log --since="2026-01-01" --until="2026-03-31" \
--grep="feat(checkout)" --oneline
# Author-specific for performance review
git log --author="Sachin" --since="6 months ago" \
--pretty=format:"%h %ad %s" --date=short | grep "feat\|fix"
Quick Reference Card
✅ feat(auth): add JWT refresh token rotation
✅ fix(checkout): handle Stripe timeout with circuit breaker
✅ refactor(UserService): extract address validation to AddressValidator
✅ perf(orders): add composite index (user_id, created_at) — 40x speedup
✅ test(PaymentService): add unit tests for refund idempotency
✅ docs(README): add local development setup instructions
✅ chore(deps): upgrade Spring Boot from 3.1.4 to 3.2.1
❌ fix
❌ WIP
❌ update code
❌ lots of changes
❌ Fixing the thing Sarah mentioned
❌ asdf
Key Takeaways
- A commit message is a letter to your future self at 3 AM during a production incident — invest in it accordingly.
- The Conventional Commits standard (feat/fix/chore/refactor) enables automated changelogs, semantic versioning, and searchable history.
git log --grepandgit bisectare only useful if your commit messages contain meaningful context, not just "fix."