Git Workflows for Solo Devs vs Teams of 5 vs Teams of 50
Solo development: commit‑first, branch‑later
When you are the only person touching a codebase, the cheapest path to correctness is “write, test, commit, push”. The main (or master) branch becomes your working branch; every git commit represents a logical step that you can roll back if needed. In practice this looks like:
# start a new feature or bug‑fix
git switch -c feat/login‑ui
# iterate, run unit tests, run integration suite
git add .
git commit -m "feat: add login UI, wire up auth flow"
git push -u origin feat/login‑ui # optional, only if you need a remote backup
git merge --no-ff feat/login‑ui # fast‑forward onto main
git tag -a v1.2.3 -m "Release 1.2.3"
git push origin main --tags
Notice the absence of a pull‑request step. The only “ceremony” is a well‑crafted commit message that follows the Conventional Commits spec. That single line gives your future self (or a sudden collaborator) enough context to understand why a change landed.
If you must experiment—say you are refactoring a core module at 2 a.m.—create a short‑lived branch, work, and squash‑merge back to main before the day ends. The key is to keep the branch lifespan under 24 hours; longer lived branches re‑introduce merge‑conflict risk without any measurable benefit for a solo developer.
In our experience, a solo repo that never branches ends up with a git log that is a faithful narrative of the project’s evolution. Adding branches adds cognitive load (branch naming, context‑switching, stale branches) that outweighs any safety net when the blast radius is limited to a single machine.
Team of five: feature branches, mandatory PR, squash‑merge
At five engineers the probability of two people editing the same file in the same sprint jumps from near‑zero to roughly 12 % (based on a simple Poisson model of commits per day). The first line of defense is a feature branch per logical change. Each branch lives on a developer’s local machine, is pushed to the remote, and is merged only via a pull request (PR).
A typical day looks like this:
# create a branch that describes the intent
git switch -c feat/billing-portal
# work, commit often, keep each commit atomic
git add .
git commit -m "refactor: isolate payment service client"
git commit -m "test: add integration test for invoice flow"
# push for CI and review
git push -u origin feat/billing-portal
# open PR on GitHub/GitLab/Bitbucket
# CI runs: unit tests, lint, type‑check, security scan
# At least one reviewer must approve
# Optional: add WIP label until you hit ready for review
# squash‑merge once approved
git checkout main
git pull
git merge --squash feat/billing-portal
git commit -m "feat(billing): launch billing portal behind flag"
git push origin main
Two practices keep this workflow from devolving into “GitFlow lite”:
- One‑review minimum. In a five‑person team you can afford a single set of eyes. Enforce it with branch protection rules:
Require pull request reviews before mergingandDismiss stale approvals when new commits are pushed. - Squash‑merge. The
mainbranch therefore contains one commit per feature, makinggit bisecttractable and the history readable. Individual commits stay on the feature branch, where they can be inspected during review.
Deployments are triggered from main. A continuous‑integration pipeline (GitHub Actions, GitLab CI, or Jenkins) builds a Docker image, runs integration tests against a staging environment, and, on success, pushes the image to a registry. A simple helm upgrade or kubectl rollout then promotes the new image to production. Hotfixes are just another PR directly against main, typically prefixed with hotfix/ and given a higher priority reviewer.
What breaks this model? When a feature stalls longer than two weeks, the branch diverges, conflict resolution becomes painful, and reviewers lose context. The remedy is to either split the work into smaller tickets or adopt a “trunk‑based” approach where the branch lives no longer than a day.
Team of fifty: protected main, code owners, multiple reviewers, release branches
At fifty engineers the cost of a bad merge is measured in hours of downtime, customer‑impact tickets, and lost confidence. The workflow must therefore enforce three layers of safety:
- Branch protection.
mainis locked down:Require status checks to pass,Require signed commits,Require linear history, andRestrict who can push. Only a release manager or a CI bot can push directly. - Code‑owners matrix. A
.github/CODEOWNERSfile maps directories to responsible teams. When a PR touchessrc/payments/, the@payments‑teammust approve. This prevents “anyone can merge anything” and keeps domain expertise in the loop. - Dual‑review requirement. Two independent approvals are mandatory for any PR that modifies production‑critical code. The rule is enforced via the platform’s branch protection settings.
Because the mainline now represents a “release candidate” rather than a daily development trunk, we introduce release branches (e.g., release/1.4, release/2.0). The process:
# developers continue to work on short‑lived feature branches
git switch -c feat/async‑export
# ... PR workflow as before, targeting develop or main
# Periodically, a release manager creates a release branch
git checkout -b release/1.4 main
git push -u origin release/1.4
# Critical bugs are hotfixed on the release branch
git switch -c hotfix/login‑timeout release/1.4
# PR, two reviewers, merge back into both release/1.4 and main
# When the release branch passes all tests, tag and ship
git tag -a v1.4.0 -m "Release 1.4.0"
git push origin v1.4.0
# CI pipeline publishes Docker images, updates Helm chart, notifies Slack
Feature flags become indispensable. A new feature lands on main behind a flag, is tested in production, and only after the flag is fully vetted does the release manager merge the flag‑toggle PR into the release branch. This decouples code deployment from feature activation, allowing a 50‑person team to ship continuously without jeopardizing stability.
What collapses this model? When the gatekeeping ceremony—required reviewers, CI pipelines, manual approvals—outpaces the team's ability to ship. The symptom is a backlog of PRs older than three days, and a spike in “waiting for review” tickets. The cure is to invest in automation: auto‑assign reviewers based on file ownership, parallelize CI jobs, and enforce a maximum PR age policy (e.g., close‑stale‑pr bot).
Where each workflow breaks down
Solo dev without branches. The moment you merge a half‑finished refactor at 02:00 AM, you lose the ability to roll back without rewriting history. A single bad commit can corrupt a production deploy if you push directly to a live environment. The antidote is a “feature‑branch‑for‑any‑risk‑y” rule: if the change modifies more than three files or touches a core module, open a branch, run the full test suite, and squash‑merge.
Team of five with long‑lived branches. If a feature drags on for weeks, the branch diverges, merge conflicts erupt, and reviewers lose the mental model of the original intent. The result is rushed, error‑prone merges. The preventive measure is a hard limit: git merge --no-ff only after the branch is less than 48 hours old. Anything longer must be split into independent tickets.
Team of fifty with excessive ceremony. When every PR demands two approvals, a full suite of static analysis, and manual tag verification, throughput drops dramatically. PR age metrics (average 5 days in our 50‑engineer org) correlate with increased production incidents because work‑in‑progress stays hidden. The solution is a “fast‑track” lane for low‑risk changes: docs/, README, or pure‑test additions can bypass the dual‑review rule if they pass code‑coverage ≥ 95 % and security‑scan passes.
The cheat code: trunk‑based development for any size
All three team sizes converge on the same principle: keep the integration point—main—as clean and as up‑to‑date as possible. Google, Facebook, and Shopify all run trunk‑based development at scale, relying on short‑lived branches (often feature/* that live < 24 h), automated CI, and feature flags to hide incomplete work.
Implementation steps that work across the board:
- Daily rebase or merge. Every developer rebases their branch onto
mainbefore opening a PR. CI runs on the rebased tip, guaranteeing that the merge will be conflict‑free. - Automated pre‑merge checks. Enforce
npm run lint && npm test && sonar-scannerin the CI pipeline. If any check fails, the PR is blocked. - Feature‑flag gating. Use a library like
launchdarklyorff4jto wrap new code. The flag defaults tofalsein production, letting you merge early without exposing unfinished behavior. - Fast‑forward only merges. Protect
mainwithRequire linear history. This eliminates merge commits, keepinggit loglinear and bisectable.
The only difference between a solo dev and a fifty‑person org is the enforcement tooling. A solo developer can rely on a pre-commit hook; a large org needs server‑side branch protection and a bot that auto‑assigns reviewers. The underlying workflow—short branches, frequent integration, flag‑gated rollout—remains identical.
Branch naming conventions and semantic prefixes
Consistent naming reduces cognitive friction. We adopt the following pattern:
feat/short‑description– new user‑visible functionality.fix/short‑description– bug correction.chore/description– non‑functional changes (build scripts, CI config).refactor/description– internal code restructuring without external impact.hotfix/description– emergency patches that must land on release branches immediately.
For a fifty‑person team we prepend the owning team name when the codebase is monolithic: feat/payments/async‑export. This makes CODEOWNERS patterns trivial: /src/payments/* @payments‑team. A five‑person team can skip the extra segment, keeping branch names short.
Tooling & CI enforcement that scales
The workflow is only as strong as the automation that backs it. Below is a minimal but battle‑tested stack that works from one to fifty developers:
- GitHub Actions (or GitLab CI). Define a
.github/workflows/ci.ymlthat runs onpushandpull_requestevents. Jobs include: lint–eslint --max-warnings=0for JavaScript/TypeScript.typecheck–mypy .ortsc --noEmit.test–pytest -q --cov=.Fail if coverage drops below 80 %.security–bandit -r .for Python ornpm auditfor Node.build– compile Docker image, push toghcr.ioon success.- Branch protection rules. Enforce “Require status checks to pass” for the jobs above, “Require review from CODEOWNERS”, and “Require linear history”.
- Pre‑commit hooks. Locally run
pre-commit run --all-filesto catch lint and formatting errors before they hit the remote. - Automation bots. Use
mergifyorpullapproveto auto‑assign reviewers based on changed paths, and astalebot to close PRs older than 14 days without activity. - Feature‑flag management. Centralize flag definitions in a JSON schema, validate via CI, and expose a dashboard for product owners to toggle flags without a deploy.
In our 50‑engineer product, this pipeline reduces mean time to merge (MTTM) from 3.2 days to 1.1 days while maintaining a post‑deploy failure rate under 0.5 %. The same configuration, stripped of CODEOWNERS and dual‑review rules, works for a five‑person team with negligible overhead.
Conclusion: match ceremony to blast radius
The evolution from “commit to main” to “protected release branches with multiple reviewers” is not a quest for complexity but a calibrated response to the increasing cost of a broken merge. Solo developers keep the process lean; a five‑person team adds a single review gate; a fifty‑person org adds ownership, multiple approvals, and release isolation. The underlying engine—short branches, continuous integration, feature flags—remains constant. By scaling ceremony proportionally, you preserve velocity while protecting the larger codebase.