Lesson 9 of 9 9 minWorkflow Essential

Git & GitHub: Writing Commit Messages That Document Your Engineering Decisions

Stop writing "fix bug" and "update code." Learn the Conventional Commits standard, the seven rules of great commit messages, and how a well-maintained Git log becomes searchable institutional memory for your team.

Reading Mode

Hide the curriculum rail and keep the lesson centered for focused reading.

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 --grep and git bisect are only useful if your commit messages contain meaningful context, not just "fix."
Recommended Prerequisites
git-mastery-01-fundamentalsgit-mastery-02-branchinggit-mastery-03-github-remotes

Premium outcome

Professional collaboration workflows, not just basic commands.

Developers who want clean collaboration habits and stronger debugging instincts.

What you unlock

  • Confidence resolving merge conflicts, rebases, and branch hygiene issues
  • A stepwise workflow for shipping clean pull requests and debugging history
  • A collaboration foundation that pays off across every engineering project

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:

  1. Separate subject from body with a blank line
  2. Limit the subject line to 72 characters
  3. Capitalize the subject line
  4. Do not end the subject line with a period
  5. Use the imperative mood in the subject line
  6. Wrap the body at 72 characters
  7. 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_THRESHOLD is 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 --grep and git bisect are only useful if your commit messages contain meaningful context, not just "fix."

Want to track your progress?

Sign in to save your progress, track completed lessons, and pick up where you left off.