Back to Git: Branching, Merge, Rebase
Q3Question 3 of 6

Git branching across a development team: feature branches, PR flow, hotfix branches

Key Concepts
  • Main is protected, always releasable, and nobody pushes to it directly -- every change goes through a pull request.
  • Short-lived feature branches (hours or days, not weeks) minimize merge conflicts and keep the team moving fast.
  • Branch protection rules require at least one approving review and passing CI checks before any merge.
  • Pull requests trigger automated CI (build, tests, static analysis) and human review before code reaches main.
  • Hotfix branches are cut from the production release tag, get an expedited review, and merge back into both release and main.
  • Squash-and-merge keeps main's history clean and readable; linking commits to ticket numbers makes history traceable for auditors.
  • Feature flags let you merge incomplete features behind a switch, avoiding long-lived branches entirely.

Definition

Branch short-lived features off main, merge them back through reviewed pull requests, and cut hotfix branches from production for urgent fixes that flow back into main.
  • Protected main branch -- the single source of truth that is always releasable; nobody pushes directly, only through reviewed pull requests.
  • Feature branch -- a short-lived branch created from main for one isolated change, named descriptively like feature/loan-interest-calculation.
  • Pull request (PR) -- a request to merge a feature branch into main, triggering automated CI checks and human code review.
  • Branch protection rules -- server-side settings that require approving reviews and passing status checks before merge.
  • Hotfix branch -- a branch cut directly from the production release tag for an urgent fix, merged back into both the release and main.
  • Trunk-based development -- a strategy where main is the trunk and feature branches are kept very short-lived.

Think of main as the official, always-correct master ledger of National Bank of India that every branch relies on. A feature branch is a teller taking a photocopy of the ledger to draft changes privately, so work-in-progress never corrupts the real ledger. A pull request is submitting that draft to a supervisor who checks it line by line. A hotfix branch is an emergency correction -- fraud is detected in the live ledger, so a senior officer fixes it on a copy of the current official version and applies it immediately.

Understanding the Concepts

What is a protected main branch?

Main is the single source of truth. It must always be releasable, and nobody pushes to it directly. You enforce this with branch protection rules that require a pull request, at least one approving review, and passing CI status checks before merge. For a bank, this is non-negotiable because main is what gets deployed to production where customer money lives.

BASH
# Configure branch protection (GitHub CLI)
gh api repos/nbi/payment-service/branches/main/protection \
  --method PUT \
  --field required_status_checks='{"strict":true,"contexts":["build","test","sonar"]}' \
  --field required_pull_request_reviews='{"required_approving_review_count":1}' \
  --field enforce_admins=true

How does feature branching work?

When a developer starts work, they create a branch off the latest main, named descriptively. They commit in isolation so incomplete code never touches main. The branch should be short-lived -- hours or a few days, not weeks -- because long-lived branches drift from main and cause painful merge conflicts.

BASH
# Create feature branch from latest main
git checkout main
git pull origin main
git checkout -b feature/loan-interest-calculation

# Work and commit
mkdir -p src/main/java/com/bank/loans
cat > src/main/java/com/bank/loans/InterestCalculator.java << 'EOF'
package com.bank.loans;

import java.math.BigDecimal;
import java.math.RoundingMode;

public class InterestCalculator {

    public BigDecimal calculateMonthlyEmi(
            BigDecimal principal, BigDecimal annualRate, int tenureMonths) {
        BigDecimal monthlyRate = annualRate
            .divide(BigDecimal.valueOf(1200), 10, RoundingMode.HALF_UP);
        BigDecimal onePlusR = BigDecimal.ONE.add(monthlyRate);
        BigDecimal power = onePlusR.pow(tenureMonths);
        return principal.multiply(monthlyRate).multiply(power)
            .divide(power.subtract(BigDecimal.ONE), 2, RoundingMode.HALF_UP);
    }
}
EOF

git add src/main/java/com/bank/loans/InterestCalculator.java
git commit -m "feat: add EMI calculation for loan service"
TEXT
[feature/loan-interest-calculation abc1234] feat: add EMI calculation for loan service
 1 file changed, 20 insertions(+)

How does the pull request flow work?

The developer pushes the branch and opens a pull request targeting main. This triggers automated CI (build, tests, static analysis) and human review. The PR is a conversation until it is approved, and only with green checks and the required approvals does it merge.

BASH
# Push and open PR
git push -u origin feature/loan-interest-calculation
gh pr create --base main \
  --title "feat: add EMI calculation for loan service" \
  --body "Adds InterestCalculator with monthly EMI formula. Covers happy path and edge cases in tests."
TEXT
Creating pull request for feature/loan-interest-calculation into main
https://github.com/nbi/payment-service/pull/42

CI checks:
  build       ✓ passed
  test        ✓ passed (18/18)
  sonar       ✓ passed (coverage 89%)

Reviewers: Pankaj (approved)
BASH
# Merge after approval (squash for clean history)
gh pr merge 42 --squash --delete-branch

How do you keep feature branches current?

While you work, main moves on as teammates merge their PRs. You regularly update your branch from main to resolve conflicts in small pieces early rather than as one huge conflict at the end.

BASH
# Update feature branch from main
git checkout feature/loan-interest-calculation
git fetch origin
git rebase origin/main

# Or merge main in (preserves branch history)
git merge origin/main

How do hotfix branches work?

When a critical bug hits production, you cut a hotfix branch from the production release tag, make the smallest possible fix, get an expedited review, and deploy fast. Then you merge the same fix back into main so it does not regress.

BASH
# Cut hotfix from production tag
git checkout -b hotfix/transfer-double-counting v2.4.1

# Make the smallest fix
cat > src/main/java/com/bank/payments/TransferIdempotency.java << 'EOF'
package com.bank.payments;

import java.util.concurrent.ConcurrentHashMap;

public class TransferIdempotency {

    private final ConcurrentHashMap<String, Boolean> processed =
        new ConcurrentHashMap<>();

    public boolean isDuplicate(String transferId) {
        return processed.putIfAbsent(transferId, Boolean.TRUE) != null;
    }
}
EOF

git add src/main/java/com/bank/payments/TransferIdempotency.java
git commit -m "hotfix: prevent double-counting on transfer retry"
git push -u origin hotfix/transfer-double-counting

# Expedited PR review and merge to release
gh pr create --base release/2.4 --title "hotfix: prevent transfer double-counting"

# CRITICAL: merge the fix back into main
git checkout main
git merge hotfix/transfer-double-counting
git push origin main

What merge strategy should you use?

Squash-and-merge collapses a feature branch into one clean commit on main, keeping history readable. A regular merge commit preserves the full branch history. Linking commits to ticket numbers makes the history traceable for auditors.

BASH
# Squash-merge: one clean commit on main
gh pr merge 42 --squash --delete-branch

# The resulting commit on main:
# abc1234 feat: add EMI calculation for loan service (#42)

# Regular merge: preserves full branch history
gh pr merge 42 --merge

# The resulting merge commit:
# def5678 Merge pull request #42 from feature/loan-interest-calculation

What about feature flags?

Feature flags let you merge incomplete features to main behind a switch, avoiding long-lived branches entirely. The code is in main but not active for users until the flag is enabled.

JAVA
package com.bank.loans;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;

@Service
public class LoanService {

    @Value("${feature.prepayment-penalty.enabled:false}")
    private boolean prepaymentPenaltyEnabled;

    public BigDecimal calculatePrepaymentCharge(
            BigDecimal outstandingAmount) {
        if (!prepaymentPenaltyEnabled) {
            return BigDecimal.ZERO; // feature not yet active
        }
        return outstandingAmount.multiply(new BigDecimal("0.02"));
    }
}
TEXT
# In dev: feature.prepayment-penalty.enabled=true
# In production: feature.prepayment-penalty.enabled=false

What are common mistakes?

The most common mistakes are long-lived feature branches that drift from main, not merging hotfixes back into main, pushing directly to main without a PR, and not linking commits to ticket numbers.

BASH
# WRONG -- pushing directly to main
git checkout main
git commit -m "quick fix"
git push origin main
# Branch protection should block this

# CORRECT -- always through a PR
git checkout -b fix/rounding-error
git commit -m "fix: correct rounding in EMI calculation [NBI-1234]"
git push -u origin fix/rounding-error
gh pr create --base main

What should a senior engineer know?

A senior engineer enforces that main always works, branches are short-lived and isolated, every change is reviewed, and urgent production fixes have a fast path that still merges back into main. They use conventional commit messages for automated changelog generation and know that the branching strategy must match the team's deployment cadence.

Visual Overview

DIAGRAM
  Git Branching Strategy
  ──────────────────────

  main (protected, always releasable)
  ──●──────●──────●──────●──────●──────●─────▶
    │      ▲      │      ▲      │      ▲
    │      │      │      │      │      │
    │  ┌───┘      │  ┌───┘      │  ┌───┘
    │  │ squash   │  │ squash   │  │ squash
    │  │ merge    │  │ merge    │  │ merge
    ▼  │          ▼  │          ▼  │
    ●──● feature/ ●──● feature/ ●──● fix/
       loan-emi      sip-returns   rounding
       (2 days)      (1 day)       (hours)

  Hotfix Flow:
  main ──●──────●──────●─────▶
         │             ▲
         │             │ merge back
  tag ───┤             │
  v2.4.1 │             │
         ▼             │
         ●──● hotfix/transfer-double-counting
            (hours, expedited review)

  PR Flow:
  ┌──────────────┐    ┌────────────────┐    ┌──────────┐
  │ Push branch   │───▶│ Open PR         │───▶│ CI checks │
  └──────────────┘    └────────────────┘    │ build ✓   │
                                             │ test  ✓   │
                                             │ sonar ✓   │
                                             └─────┬────┘
                                                   │
                                             ┌─────▼────┐
                                             │ Review +   │
                                             │ Approve    │
                                             └─────┬────┘
                                                   │
                                             ┌─────▼────┐
                                             │ Merge to   │
                                             │ main       │
                                             └───────────┘

Key Takeaway

Managing Git branching starts with a protected main branch that is the single source of truth and always releasable. Nobody pushes directly -- every change goes through a pull request with automated CI checks and at least one human review. This gives peer review, an audit trail, and a gate that stops broken code from reaching production.

The everyday workflow is short-lived feature branching. A developer creates a branch from main, commits in isolation, keeps the branch short-lived to avoid drift, and opens a PR when ready. Squash-merging keeps main's history clean and readable, and linking commits to ticket numbers makes everything traceable for auditors.

Hotfixes get a fast, well-defined path. When a critical bug hits production, you cut a hotfix branch from the release tag, make the smallest fix, get an expedited review, and deploy immediately. The crucial step is merging that fix back into main so the bug never regresses.

Remember one sentence: main is always releasable, feature branches are short-lived and merged through reviewed PRs, and hotfixes flow back into main so no fix is ever lost.

---

Share
Use ← → arrow keys to navigate