Git Workflows That Actually Work for Small Teams
Trunk-based, feature branches, and the hybrid approach I settled on after leading frontend across four major projects.
I've used git flow, trunk-based development, and several workflows in between. After leading frontend on four major projects with teams of two to eight developers, I've settled on a hybrid that works for small teams shipping web applications. Not startups-in-a-garage small, and not enterprise-with-release-trains large. The in-between.
Why Git Flow Didn't Work
Git flow is the workflow with main, develop, feature/*, release/*, and hotfix/* branches. It was designed for software with versioned releases like desktop apps, libraries, mobile apps with app store review cycles.
For web applications deployed on every merge, it's overhead. The develop branch becomes a staging area nobody looks at. Release branches add ceremony to what should be a continuous process. Hotfix branches create merge conflicts with whatever's on develop.
We tried it on my first major project. Within a month, we'd abandoned release/* branches and were merging features directly to develop, which was just a slower version of merging to main.
Why Pure Trunk-Based Was Too Fast
The opposite extreme: everyone commits to main, maybe behind feature flags. Google and Meta do this. It works because they have deep CI, heavy test coverage, and code review infrastructure that most small teams don't have.
On a five-person team where test coverage is good-but-not-great and reviews happen in hours not minutes, pushing directly to main means:
- Broken deploys when someone pushes a half-finished feature
- Pressure to review quickly rather than review well
- Feature flags for everything, which become their own maintenance burden
The Hybrid That Works
Here's what I use now:
One permanent branch: main. It's always deployable. This is non-negotiable.
Short-lived feature branches. Named feat/thing, fix/thing, or refactor/thing. They live for one to three days. If a branch lives longer than a week, something is wrong: the scope is too large, the developer is blocked, or the feature needs to be broken into smaller pieces.
Pull requests with one reviewer. Not two, not three. One person who understands the area of code being changed. The review should happen within a few hours. If it takes longer, the PR is probably too large.
Squash merge to main. The feature branch can have messy commits (WIP, fixups, "actually fix the thing"). When it merges, it becomes one clean commit with a meaningful message. The branch history is noise; the main history tells a story.
Deploy on merge. No staging branch, no release process. Every merge to main triggers a deploy. If something breaks, fix forward or revert the commit. This only works if you have:
- A CI pipeline that runs before merge (tests, lint, type check)
- A deploy process that takes minutes, not hours
- Monitoring that tells you quickly when something is wrong
Branch Naming That Helps
I keep it simple:
feat/user-avatar-upload
fix/login-redirect-loop
refactor/extract-api-client
chore/update-dependencies
The prefix communicates intent at a glance. In a list of branches, you can immediately see what's a feature, what's a fix, and what's housekeeping.
Commit Messages
I use conventional commits on main (via squash merge messages) but don't enforce them on feature branches. The feature branch is the developer's workspace. Let them commit however they think. The squash merge message is what matters.
feat: add user avatar upload with S3 presigned URLs
Implemented avatar upload flow with client-side image validation,
S3 presigned URL generation, and optimistic UI update. Added
server-side MIME type verification and size limit enforcement.
The first line is a conventional commit. The body explains what and why. This format is parseable by changelog generators and readable by humans.
Handling Conflicts
The number one source of conflicts in small teams: long-lived branches. Two developers working on the same area for a week will have painful merges.
Prevention:
- Keep branches short. One to three days.
- Rebase on main daily. Not merge, rebase. This keeps your branch's changes on top of the latest main, making the final merge trivial.
- Communicate. If two people need to change the same file, one goes first. This is a human problem, not a git problem.
When conflicts do happen:
git fetch origin
git rebase origin/main
# resolve conflicts
git add .
git rebase --continue
Rebase replays your commits on top of the updated main. Each conflict is resolved in the context of a single commit, which is easier to reason about than a merge conflict that spans multiple changes.
What I'd Tell a Team Lead
Start with this workflow. Don't customize it until you feel specific pain. Most small teams overengineer their git workflow before they've shipped enough to know what actually causes friction.
The workflow should be invisible. If developers are spending more than five minutes a day thinking about git (outside of actual code review), the process is too complex. Simplify until it disappears into the background.