Every team reinvents Git workflows. Most end up with something that worked for three people but breaks at fifteen. Here’s what actually scales.
The Problem With “Whatever Works”
Small teams can get away with anything. Push to main, YOLO merges, commit messages like “fix stuff” — it all works when you can shout across the room.
Then the team grows. Suddenly:
- Two people edit the same file and spend an hour on merge conflicts
- Nobody knows what’s in production vs staging
- “Which commit broke this?” becomes an archaeological dig
- Releases are terrifying because nobody’s sure what changed
The solution isn’t more process. It’s the right process.
Trunk-Based Development
The simplest workflow that scales. Everyone commits to main (trunk) with short-lived branches.
Rules
- Branches live < 2 days — If it takes longer, break it into smaller pieces
- Main is always deployable — Tests pass, builds work
- Feature flags hide incomplete work — Ship code, enable later
- No long-running branches — No
develop, norelease/v2
Why It Works
Short branches mean small diffs. Small diffs mean:
- Easier code review
- Fewer merge conflicts
- Faster feedback
- Lower risk per change
| |
When It Doesn’t Work
- Teams without CI/CD (you need fast feedback)
- Compliance requirements mandating release branches
- Very junior teams without code review discipline
GitHub Flow
A slight variation: main + feature branches + PRs. No develop branch, no release branches.
The Process
- Create branch from
main - Make commits
- Open Pull Request
- Discuss, review, test
- Merge to
main - Deploy
| |
PR Best Practices
Small PRs:
| |
Descriptive commits:
| |
GitFlow (When You Need It)
More complex, but necessary for some contexts: packaged software, mobile apps, strict release schedules.
Branches
main: Production code onlydevelop: Integration branchfeature/*: New features, branch from developrelease/*: Release prep, branch from develophotfix/*: Emergency fixes, branch from main
When To Use
- Mobile apps with App Store review delays
- On-premise software with versioned releases
- Regulated industries requiring release documentation
- Teams that deploy monthly, not daily
When To Avoid
- Web apps with continuous deployment
- Teams under 10 people
- Microservices (each service is its own repo/workflow)
Commit Message Conventions
Consistent commits make history useful. Conventional Commits is the standard:
Types
| |
Examples
| |
Enforcing With Hooks
| |
Or use commitlint with Husky:
| |
Branch Protection
GitHub/GitLab branch protection prevents accidents:
| |
This ensures:
- No direct pushes to main
- All changes reviewed
- Tests pass before merge
- Even admins follow the rules
Rebasing vs Merging
Merge commits preserve history:
Rebasing linearizes history:
My Recommendation
- Rebase feature branches before merging (clean history)
- Merge with merge commits to main (preserve context)
- Never rebase shared branches
| |
Monorepo Considerations
When multiple services live in one repo:
Path-Based CI
Only run tests for changed paths:
| |
CODEOWNERS
Route reviews to the right teams:
The Actual Advice
- Start with trunk-based — Add complexity only when needed
- Enforce via automation — Branch protection, CI checks, commit hooks
- Keep branches short — Days, not weeks
- Make main always deployable — Feature flags are your friend
- Document your workflow — A simple CONTRIBUTING.md saves arguments
The best workflow is the one your team actually follows. Start simple, add rules when you feel pain, and automate everything you can. 🌍