A commit message is a small note to whoever reads your code history next. Sometimes, that person is you, three months later, trying to understand why something changed.
Messages like update code or fix stuff don’t leave much to work with.
Conventional Commits gives those messages a simple, consistent structure. Here’s how it works, and where it helps.
The basic format
type(scope): description
For example:
feat(goals): add a daily learning target
There are three parts:
- Type: the kind of change.
- Scope: the part of the project affected. This is optional.
- Description: a short explanation of what changed.
You can also add a body when the change needs more context.
Types you’ll commonly see
| Type | Typical use | Example |
|---|---|---|
feat |
A new feature | feat(dashboard): add weekly learning totals |
fix |
A bug fix | fix(timer): stop tracking when the tab loses focus |
docs |
Documentation | docs: explain how to install the extension |
refactor |
Restructuring without changing behavior | refactor(timer): extract session calculations |
test |
Test changes | test(streaks): cover consecutive learning days |
chore |
Maintenance | chore: update development dependencies |
The specification defines feat and fix; other types are allowed, so teams can agree on additional categories that suit their work.
These are illustrative examples for a learning tracker, rather than a list of actual commits.
The description still does most of the work
Compare:
fix: fix timer
with:
fix(timer): exclude time spent in background tabs
Both follow the format. The second tells you what behavior changed.
A useful question before committing is:
Could someone understand the purpose of this change without opening the diff?
For changes involving a tradeoff or a surprising decision, add the reasoning in the body:
fix(streaks): count learning days independently of daily goals
A learner can study without reaching their daily target.
Use tracked activity to determine whether the day contributes
to their streak.
That explanation preserves a product decision that the code alone might not make obvious.
What about breaking changes?
A breaking change requires consumers to adapt their existing usage. You can mark it with ! before the colon and explain the migration in a footer:
feat(config)!: replace trackingInterval with trackingIntervalMs
BREAKING CHANGE: Rename trackingInterval to trackingIntervalMs
and provide its value in milliseconds instead of seconds.
Under the convention’s Semantic Versioning mapping, fixes correspond to patch releases, features to minor releases, and breaking changes to major releases. Release automation needs to be configured to apply those rules.
Why use this?
A consistent structure can help people scan project history. It also gives tools predictable information for generating changelogs and determining version bumps.
You can start by writing messages manually. A commit-message linter or release pipeline can come later if it solves a problem in your workflow.
Where the convention falls short
A Hacker News discussion about Conventional Commits raised a useful concern: it’s possible to spend more effort categorizing a change than explaining it.
Adding fix: to an unclear message doesn’t make it informative. And a generated changelog may still need editing before it helps users understand a release.
My takeaway is to keep the format simple and put care into the description and reasoning. The convention provides structure; the writer provides context.
For your next commit, try:
type: describe the specific change
Add a scope if it helps, and a body if there’s reasoning worth preserving.
Do you use Conventional Commits in your projects? Has the structure helped you, or do you prefer another approach?
Top comments (0)