Conventional Commits and Reasons for Code Change

M. Feathers identifies four reasons to change code in Working Effectively with Legacy Code: adding a feature, fixing a bug, improving the design, and optimizing resource usage. A healthy codebase exercises all four. When one type dominates β€” usually bug fixes β€” earlier changes were likely undisciplined: each fix without a test creates a seam for the next bug.

Conventional commit prefixes track which type of change a commit represents. The standard vocabulary (feat, fix, docs, style, refactor, test, chore) is larger than it needs to be. All of it collapses to three prefixes once you anchor classification to the contract.

Contract: what the code promises its outermost audience. For a library, the audience is callers. For a product, it is end-users. The contract includes inputs accepted, outputs produced, errors raised, externally visible side effects, type signatures, documented behavior, and the implicit safety promises every system makes: no data leaks, no crashes on malformed input, no privilege escalation. Speed, memory use, internal structure, and log or metric format (unless explicitly documented as stability surfaces) sit outside the contract.

With that definition, prefix selection reduces to three questions asked in order. Stop at the first yes.

  1. Was the contract violated before this change, and is it now honored? β†’ fix
  2. Does this change alter what the contract promises β€” adding, removing, or modifying what callers or users can rely on? β†’ feat
  3. Otherwise β†’ chore

The Feathers reasons map cleanly onto this test:

  • Fixing a bug that broke a promise the code was supposed to keep β†’ fix
  • Adding a feature that expands what the code promises β†’ feat
  • Improving the design without changing the promise β†’ chore
  • Optimizing resource usage without changing the promise β†’ chore

chore is the default. Most changes β€” refactoring, performance work, dependency updates, internal documentation, tests, migrations, i18n translation strings β€” sit below the contract line. feat and fix are reserved for changes that cross it. Reserving them keeps the prefix log scannable: for a library, a feat commit tells downstream callers they may need to update; for a product, it tells the team that a promise to users changed. A fix commit signals that a violated promise is now honored. If every commit is labeled feat, neither signal carries information.

Worked examples

Contract violations repaired (fix):

  • A security patch closes a credential leak in the token handler. The implicit safety promise (no data leaks) was violated; it is now honored.
  • An error message referenced a flag removed two releases ago. Error text is part of the contract.

Contract extended or narrowed (feat):

  • A return type tightened from any to User. Type signatures are part of the contract; callers relying on any may need to update.
  • A deprecated /v1 endpoint is removed. The contract narrowed.

Below the contract (chore):

  • A cache cuts user-lookup time from 50ms to 2ms. Speed sits outside the contract; the promise did not change.
  • A backward-compatible database index is added. The migration is invisible to callers.

Reverts: apply the three-question test to the revert’s effect on the contract.

  • Reverting a buggy release restores a promise that was being violated β†’ fix.
  • Pulling a feature that was shipped removes something callers could rely on β†’ feat.

If choosing a prefix is still unclear after asking the three questions, the commit likely contains more than one concern. Split it.

i18n example

Adding i18n to a product involves two kinds of work that belong in separate commits, and ideally separate pull requests.

Translation strings sit below the contract line. Adding them does not change what the application promises users: no new mechanism, no new user-facing behavior. That work is chore.

A language switcher is a new user-facing mechanism. It adds a promise: users can now select a language. That crosses the contract line β†’ feat.

Two PRs:

  • chore: i18n library setup and translation keys
  • feat: language switcher and initial locale support

The split keeps each PR reviewable on its own terms. The chore PR changes no contract; a reviewer can focus on the wiring. The feat PR carries the user-facing change; a reviewer can focus on the contract it introduces.

Tracking prefixes across commits makes the distribution of change types visible. A log dominated by fix suggests the contract is violated frequently. Feathers reads that pattern as a sign of system decay: each undisciplined fix makes the next one more likely. A log with no chore suggests design and performance work is deferred or hidden inside feat commits. The prefix is the signal; it only works if used precisely.

Something to read next: Awesome Pull Requests
Β© 2026 Vadim Kotov Β· vadirn.io