What a Good Commit Actually Looks Like

by Eric Hanson, Backend Developer at Clean Systems Consulting

The Anatomy of a Real Commit

Your team's code review process is slowing down. PRs sit for days. Reviewers leave comments like "why was this changed?" and "what's the context here?" The diff is there, the code makes sense in isolation, but the reviewer doesn't know what problem it's solving. Half the review time is actually archaeology — reconstructing the intent from the code.

Most of that overhead disappears with well-structured commits. Not just better messages — better commits. The message is one piece. The scope, atomicity, and structure of the change are equally important.

What Atomic Actually Means in Practice

An atomic commit captures one logical unit of change. It's independently understandable, independently revertable, and leaves the codebase in a valid state.

"One logical unit" is not the same as "one file" or "one function." It's the smallest meaningful slice of intent.

Refactoring a class and adding a feature are two logical units. They should be two commits, even if the feature required the refactor. Fixing a bug and writing its regression test are one logical unit. They belong together.

Here's the test: if a future developer runs git revert <sha>, what happens? If reverting one thing also silently removes something unrelated, you bundled two logical changes into one commit.

# Working directory has mixed changes:
# - Refactored UserRepository to use JPA Criteria API
# - Added address validation feature that uses the refactored repo

# Stage and commit them separately:
git add src/repository/UserRepository.java \
        src/repository/UserRepositoryImpl.java
git commit -m "refactor: migrate UserRepository to JPA Criteria API

Reduces query complexity and eliminates raw string SQL. No behavior
change — all existing tests pass."

git add src/service/AddressValidationService.java \
        src/service/AddressValidationServiceTest.java
git commit -m "feat: add address validation before order submission

UK postcodes were being accepted in US-only address fields, causing
failed shipments. Added format validation per ISO 3166-1 country
codes. Validated against AddressValidationServiceTest."

A Real Commit Message, Annotated

The Conventional Commits specification (conventionalcommits.org) gives the message a consistent, machine-readable structure. Here's a full example:

fix(payment): prevent duplicate charges on gateway timeout

The payment gateway occasionally returns HTTP 504 after successfully
processing a charge. Our retry logic treated the timeout as a failure
and submitted a second charge, resulting in duplicate billing.

Fixed by adding an idempotency key derived from order_id + attempt
number to every charge request. The gateway deduplicates requests
with the same idempotency key within a 24-hour window.

Affected flow: POST /api/v2/orders/:id/pay → PaymentGatewayClient#charge

Refs: incident-2026-03-14, #PRD-1841
Fixes: #2047

Breaking it down:

  • fix(payment): — type and scope. Enables automated changelogs. Types: feat, fix, perf, refactor, chore, docs, test.
  • First line — present tense, imperative, under 72 characters, explains the what.
  • Blank line — required separator before body.
  • Body — explains the why and the context. The diff shows what changed; the body explains why that change was the right fix.
  • Footer — ticket references. These enable your issue tracker to cross-link commits automatically.

The Boundary Case: How Big Is Too Big

A commit is too big when:

  1. The subject line requires "and" to describe what it does
  2. The changes span multiple unrelated areas of the system
  3. You can't summarize it in 72 characters without losing something important
# Too big — two separate concerns crammed together
feat: add OAuth login and fix dashboard pagination bug

# Should be two commits:
feat(auth): add OAuth2 login via Google and GitHub
fix(dashboard): correct off-by-one in pagination when total < page_size

A commit is too small when it's a fixup of another commit on the same branch that hasn't been merged yet. Three commits that say "add test", "fix test", "actually fix test" should be one commit via git rebase -i before merging.

Code Examples That Belong in Commit Messages

For complex changes, a short code snippet in the commit body is worth more than paragraphs of prose:

refactor(cache): replace synchronized block with ConcurrentHashMap

The synchronized block on the cache map was a bottleneck under
high read concurrency. Replaced with ConcurrentHashMap#computeIfAbsent
which uses segment-level locking.

Before (under 200 concurrent reads on 32-core machine):
  p99 latency: 340ms

After (same conditions):
  p99 latency: 12ms

No API changes. Thread safety verified by CacheConcurrencyTest.

You don't need this level of detail for a two-line bug fix. You do need it when the change has non-obvious performance implications or when the alternative approach was deliberately rejected.

What Good Looks Like Across Commit Types

Feature commit:

feat(invoicing): add PDF export for invoice history

Customers on Enterprise plan have been requesting downloadable
invoices for accounting systems. Generates PDF via iText 8
with company branding and line-item breakdown.

New endpoint: GET /api/v1/invoices/:id/pdf
Requires: invoice:export permission
Rate limited: 60 requests/hour per account

Chore/dependency update:

chore(deps): upgrade Spring Boot 3.1.x → 3.3.2

Picks up CVE-2024-22257 fix (Spring Security) and Micrometer 1.13
with virtual thread support. No API changes required.

Tested: full integration test suite passes locally.
Migration notes: none for current usage patterns.

Revert:

revert: revert "perf: increase user cache TTL to 24h"

This reverts commit a3f9d24. The 24h TTL caused stale permission
data in staging — users retained access after role revocation.
Reverting to 15m TTL while we investigate a targeted cache
invalidation approach.

The Workflow That Makes This Stick

Good commits require staging discipline. If you always git add . and commit everything at once, you can't be atomic. The tools that help:

  • git add -p — stages individual hunks, not files. Lets you separate unrelated changes in the same file.
  • git commit --amend — rewrites the last commit. Use it before pushing to fix messages or add a missed file.
  • git rebase -i origin/main — squash fixup commits, reorder, reword. Run this before every PR.

The goal is not ceremonial rigor. It's a commit history that a developer can navigate two years from now without having to ping anyone for context. That's the bar.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

Disagreeing With a Technical Decision Without Burning Bridges

Technical disagreements are inevitable in engineering teams. How they are handled determines whether the team's technical quality improves over time or whether decisions get made by whoever argues longest.

Read more

Why Building Software Is More Expensive Than Most Founders Expect

Wait… why is this so expensive? It’s just an app.” That moment hits almost every founder at some point.

Read more

The Onboarding Process That Makes Clients Feel Confident From Day One

The first week of a new engagement sets the tone for everything that follows. Most contractors waste it. The ones who do not build confidence faster than any proposal or portfolio ever could.

Read more

Ruby Performance Tips I Learned the Hard Way on a Production System

Most Ruby performance advice is synthetic benchmark folklore. These are patterns that caused measurable production problems — and the specific changes that fixed them.

Read more