Skip to main content

GitFlow Branching Strategy

Comprehensive guide to our GitFlow-based branching model for managing parallel development, releases, and hotfixes using GitLab.

Overview

We use GitFlow, a branching model designed for projects with scheduled releases. It provides a robust framework for managing features, releases, and hotfixes while maintaining a stable production codebase.

Why GitFlow?

GitFlow provides clear separation between production-ready code, work in progress, and features under development through a structured branching model. Unlike trunk-based development where all changes merge directly to a single main branch, GitFlow introduces an integration layer (develop) that isolates main from potentially unstable features. This separation enables:

  • Parallel release management: Multiple release branches can exist simultaneously for supporting different versions (e.g., v1.5.0 in production while v1.6.0 is in staging)
  • Long-term version maintenance: Bug fixes can be applied to specific release branches without pulling in unrelated changes from develop
  • Rapid production fixes: Hotfix branches from main bypass the standard feature workflow, enabling emergency patches to reach production in hours instead of waiting for the next scheduled release
  • Audit trail for compliance: Every production change is traceable through merge commits, tags, and branch history - critical for regulatory requirements in financial services

Relationship to other guides:


Branch Types

Permanent Branches

These branches exist for the lifetime of the project and are never deleted.

BranchPurposeContentsProtection
mainProduction-ready codeOnly stable, tested, released codeProtected (2 approvals required)
developIntegration branchLatest development changes for next releaseProtected (1 approval required)

Key principles:

  • Direct commits forbidden: All changes to main or develop must go through merge requests. This ensures code review occurs before integration and creates a traceable history of who approved what changes. GitLab enforces this through branch protection rules that reject direct pushes at the server level.
  • Main reflects production: The HEAD of main always points to the commit currently deployed in production. This allows teams to instantly see production state and simplifies rollback procedures (git checkout to the previous tag).
  • Develop is deployable: While develop contains the latest features, it must remain in a deployable state at all times. Broken builds on develop block all feature merges, creating immediate pressure to fix issues. This is enforced through CI/CD quality gates that prevent merging if tests fail.

Temporary Branches

These branches are created for specific purposes and deleted after merging.

Branch PrefixPurposeBranches FromMerges ToLifetimeExample
feature/*New featuresdevelopdevelop2-7 daysfeature/JIRA-1234-payment-retry
bugfix/*Non-critical bug fixesdevelopdevelop1-3 daysbugfix/JIRA-5678-fix-validation
release/*Release preparationdevelopmain + develop1-3 daysrelease/v1.5.0
hotfix/*Emergency production fixesmainmain + developHours-1 dayhotfix/v1.4.1-fix-critical-bug

GitFlow Workflow Visualization

Complete GitFlow Diagram

Branch Lifecycle Flows

Feature Development Flow

Release Flow

Hotfix Flow


Detailed Workflows

1. Feature Development

When to use: Developing new features or non-critical bug fixes

Workflow overview:

  1. Create feature branch from latest develop
  2. Develop feature with small, logical commits
  3. Keep feature branch updated with develop (rebase regularly)
  4. Create merge request when ready
  5. Address code review feedback
  6. Squash and merge to develop
  7. Delete feature branch

Best practices:

  • Keep features small and focused (2-5 days maximum): Short-lived branches reduce merge conflicts because they diverge less from the base branch. The probability of conflicts increases exponentially with branch age as more developers merge changes to develop. Small features also improve code review quality - reviewers can thoroughly examine 200 lines but tend to rubber-stamp 2000+ line changes.
  • Commit frequently with conventional commit messages: Frequent commits (every hour or two) create savepoints you can return to if you take a wrong direction. Conventional commit format enables automated changelog generation and semantic version bumping (feat→MINOR, fix→PATCH, BREAKING CHANGE→MAJOR).
  • Rebase on develop before creating MR: This ensures conflicts are resolved by the feature developer (who understands the changes) rather than the merge approver. Rebasing also creates a linear history that's easier to review and understand.
  • Create draft MR early for feedback: Draft MRs (marked with "WIP" or "Draft:") enable early architectural review before significant work is invested. This catches design issues when they're cheap to fix.
  • Add comprehensive tests: Feature branches must include tests achieving ≥85% code coverage and ≥80% mutation coverage. Tests serve as living documentation and prevent regressions when other developers modify related code.
  • Don't let feature branches live >1 week: Week-old branches accumulate massive conflicts and become "fear branches" that developers avoid updating. If a feature genuinely requires >1 week, break it into smaller incremental features.
  • Don't mix multiple unrelated changes: Each feature branch should address one concern. Mixed changes make code review difficult and prevent selective reversion if one change causes issues.

See: Git Daily Workflow for detailed commands

2. Release Process

When to use: Preparing a new production release

Workflow overview:

  1. Create release branch from develop (e.g., release/v1.5.0)
  2. Update version numbers and CHANGELOG
  3. Deploy to staging environment
  4. QA testing and bug fixes (only in release branch)
  5. Merge to main with tag
  6. Deploy to production
  7. Merge back to develop
  8. Delete release branch

Release cadence:

  • Major releases (v2.0.0): Quarterly (breaking changes)
  • Minor releases (v1.5.0): Monthly (new features)
  • Patch releases (v1.4.1): As needed (bug fixes via hotfix)

Release checklist:

  • All planned features merged to develop
  • Version numbers updated (package.json, build.gradle, etc.)
  • CHANGELOG.md updated with release notes
  • Database migrations tested
  • Staging deployment successful
  • QA sign-off obtained
  • Security scan passed
  • Performance testing completed
  • Rollback plan documented
  • Stakeholders notified

See: Git Daily Workflow for step-by-step commands

3. Hotfix for Production

When to use: Critical bugs in production that can't wait for next scheduled release

Criteria for hotfix:

  • Critical bugs affecting all/most users
  • Security vulnerabilities
  • Data corruption issues
  • Payment processing failures
  • Regulatory compliance violations
  • No Feature requests (use feature branch)
  • No Minor bugs (wait for next release)
  • No Performance improvements (unless critical)

Workflow overview:

  1. Create hotfix branch from main
  2. Fix the issue with minimal changes
  3. Update version (patch increment)
  4. Create urgent MR (expedited review)
  5. Merge to main and tag
  6. Deploy to production immediately
  7. Merge to develop
  8. Delete hotfix branch

Hotfix approval requirements:

  • 2 reviewers (expedited approval within 2 hours)
  • Security team review (if security-related)
  • Product Owner notification
  • Stakeholder communication plan
Production Hotfixes

Hotfixes bypass the normal release cycle. Use responsibly!

  • Document the reason for hotfix in commit message
  • Include Jira ticket reference
  • Notify team immediately
  • Schedule post-mortem for critical issues

See: Git Daily Workflow for commands


Commit Message Conventions

We follow Conventional Commits specification to enable automated tooling and maintain clear project history. This standardized format allows CI/CD systems to parse commits programmatically for automatic semantic versioning (determining whether a release should be MAJOR, MINOR, or PATCH based on commit types), changelog generation, and release note creation. The structured format also improves git log readability and enables filtering by type (e.g., git log --grep="^feat:" to see all features).

Format

<type>(<scope>): <subject>

[optional body]

[optional footer]

Commit Types

TypeUsageSemantic Version ImpactExample
featNew featureMINORfeat(payments): add retry mechanism
fixBug fixPATCHfix(auth): resolve token expiration issue
docsDocumentation-docs(api): update payment endpoint docs
styleCode formatting-style(ui): format React components
refactorCode restructuring-refactor(services): extract validation logic
perfPerformance improvementPATCHperf(db): optimize payment query with index
testAdding tests-test(payments): add integration tests
choreMaintenance-chore(deps): update Spring Boot to 3.2.0
ciCI/CD changes-ci(pipeline): add security scanning
revertRevert previous commit-revert: revert "feat(payments): add retry"

Scope

Optional, indicates area of codebase:

  • payments, auth, customers, accounts, loans, cards
  • api, ui, db, security, config

Subject Guidelines

  • Use imperative mood ("add" not "added" or "adds")
  • Don't end with a period
  • Maximum 72 characters
  • Capitalize first letter
  • Be descriptive but concise

Breaking Changes

Indicate breaking changes in footer:

feat(api): change payment endpoint response format

BREAKING CHANGE: Payment response now returns `transactionId` instead of `paymentId`.
Clients must update to use new field name.

Refs: JIRA-1234

Examples:

# Feature with detailed body
feat(payments): add exponential backoff for retry logic

Implements exponential backoff with configurable delay and max attempts.
Prevents overwhelming external payment gateway during outages.

- Configurable via application.properties
- Defaults to 3 retries with 2s, 4s, 8s delays
- Added comprehensive unit and integration tests

Closes JIRA-1234

# Bug fix with security implications
fix(auth): prevent token theft via XSS

- Add HttpOnly flag to authentication cookies
- Sanitize user input in profile page
- Add Content-Security-Policy header
- Update security tests

Fixes CVE-2024-1234

# Performance improvement
perf(db): add index on payment_transactions.customer_id

Query time reduced from 450ms to 12ms for customer payment history.
Improves dashboard load time significantly.

Refs: JIRA-5678

See: Git Daily Workflow for more examples


Branch Naming Conventions

Format

<type>/<ticket-id>-<short-description>

Examples

Good naming:

  • feature/JIRA-1234-add-payment-retry
  • feature/JIRA-5678-improve-error-messages
  • bugfix/JIRA-9012-fix-null-pointer-exception
  • release/v1.5.0
  • hotfix/v1.4.1-fix-payment-crash

Poor naming (avoid):

  • feature/new-feature (no ticket reference)
  • fix-bug (no type prefix, no ticket)
  • JIRA-1234 (no type, no description)
  • johns-branch (no context)
  • temp (meaningless)

Branch Name Length

  • Keep total length under 50 characters
  • Description should be clear but concise
  • Use hyphens to separate words (kebab-case)

Version Numbering

We follow Semantic Versioning: MAJOR.MINOR.PATCH

Semantic versioning provides a contract between software maintainers and consumers. Each number increment signals the type of changes and helps consumers understand upgrade risks without examining changelogs. MAJOR increments signal breaking changes (incompatible API modifications), MINOR increments indicate new backward-compatible functionality, and PATCH increments represent backward-compatible bug fixes. This allows automated dependency management tools to safely update PATCH versions automatically while requiring manual review for MAJOR versions.

Versioning Rules

MAJOR version (v2.0.0) - Incompatible API changes:

  • Breaking changes to REST API contracts
  • Database schema changes requiring data migration
  • Removal of deprecated features
  • Changes to authentication/authorization model

MINOR version (v1.5.0) - Backward-compatible functionality:

  • New API endpoints
  • New features
  • Database schema additions (backward compatible)
  • New optional configuration

PATCH version (v1.4.1) - Backward-compatible bug fixes:

  • Bug fixes
  • Security patches
  • Performance improvements (no API changes)
  • Documentation updates

Pre-release Versions

Use for testing before official release:

  • v1.6.0-alpha.1: Early development testing
  • v1.6.0-beta.1: Feature complete, testing in progress
  • v1.6.0-rc.1: Release candidate, final testing before production

Version Update Locations

Update version in all relevant files:

  • package.json (Node.js projects)
  • build.gradle (Java/Kotlin projects)
  • pubspec.yaml (Flutter projects)
  • Info.plist / build.gradle (Mobile apps)
  • CHANGELOG.md

GitLab Branch Protection

Protection Rules for main

Configure in Settings > Repository > Protected Branches:

Branch: main
Settings:
Allowed to merge: Maintainers only
Allowed to push: No one
Allowed to force push: Disabled
Code owner approval: Required
Approvals required: 2
Eligible approvers: Maintainers, Tech Leads
Reset approvals on new push: Yes
All discussions must be resolved: Yes
Pipelines must succeed: Yes

Quality gates for main:

  • All tests passing (unit, integration, E2E)
  • Code coverage ≥85%
  • Mutation coverage ≥80% (Java), ≥75% (TypeScript)
  • Security scans clean (no critical/high vulnerabilities)
  • 2 reviewer approvals
  • All MR discussions resolved

Protection Rules for develop

Branch: develop
Settings:
Allowed to merge: Developers + Maintainers
Allowed to push: No one
Allowed to force push: Disabled
Code owner approval: Optional (for owned code)
Approvals required: 1
Eligible approvers: Any developer
Reset approvals on new push: Yes
All discussions must be resolved: Yes
Pipelines must succeed: Yes

Quality gates for develop:

  • All tests passing
  • Code coverage ≥85%
  • Linting passes
  • Build succeeds
  • 1 reviewer approval

Protection Rules for release/*

Branch pattern: release/*
Settings:
Allowed to merge: Maintainers
Allowed to push: Maintainers
Allowed to force push: Disabled
Approvals required: 2
Eligible approvers: Tech Leads, Senior Developers

Code Owners Configuration

Create .gitlab/CODEOWNERS file in repository root:

# Default owners for everything
* @platform-team

# Payment processing requires payment team approval
/src/payments/ @payment-team-lead @payment-senior-dev
/src/models/payment/ @payment-team-lead

# Security-critical components
/src/security/ @security-team
/src/auth/ @security-team

# API contracts require API team approval
**/openapi.yaml @api-team
**/api-spec.yaml @api-team

# Infrastructure and pipelines
/.gitlab-ci.yml @devops-team
/infrastructure/ @devops-team

# Database migrations require DBA approval
**/db/migration/ @database-team @backend-lead

How code owners work:

GitLab parses the CODEOWNERS file when a merge request is created, matching changed file paths against the glob patterns defined in the file. The matching is evaluated from bottom to top, with later matches overriding earlier ones (allowing for broad defaults followed by specific overrides). For example, if a file matches both * (all files) and /src/payments/* (payment files), the more specific payment team ownership takes precedence.

Technical implementation:

  1. MR creation triggers CODEOWNERS file parsing on the target branch (e.g., develop)
  2. GitLab evaluates each changed file path against patterns using glob matching (supports *, **, and directory paths)
  3. Matched owners are automatically added as required reviewers in the MR
  4. If branch protection has "Code owner approval required" enabled, GitLab prevents merge until at least one owner from each matched pattern approves
  5. Owners receive email/notification when code they own is modified in an MR
  6. Approval counts are tracked separately for code owners vs regular reviewers

See: Migration Guide for setup details


Merge Strategies

Squash and Merge (Default for Features)

Use for: feature/*develop, bugfix/*develop

Squashing combines multiple commits into a single commit by creating a new commit with the combined diff of all commits. Git creates this new commit on the target branch (develop) with a fresh commit SHA, effectively collapsing the feature branch's commit history. This differs from a regular merge which preserves all individual commits and their SHAs. The original feature branch commits remain in the reflog but are eventually garbage collected when the feature branch is deleted.

Advantages:

  • Clean, linear history on develop
  • One commit per feature (easy to understand with git log --oneline)
  • Easy to revert entire feature with single git revert command
  • Removes work-in-progress commits, typo fixes, and other noise from main history
  • Reduces visual clutter in git log graphs and simplifies bisecting for bugs

Before squash (messy):

feat(payments): add retry logic
test(payments): add initial tests
fix(payments): fix typo in test
test(payments): add edge case tests
fix(payments): address review feedback
docs(payments): update API docs

After squash (clean):

feat(payments): add retry mechanism with comprehensive tests

Implements exponential backoff retry logic for failed payments.
Includes unit tests, integration tests, and API documentation.

Closes JIRA-1234

Merge Commit (No Fast-Forward)

Use for: release/*main, release/*develop, hotfix/*main

A merge commit is a special commit with two (or more) parent commits, created when Git combines branches. The --no-ff flag forces creation of a merge commit even when Git could fast-forward (simply move the branch pointer). This merge commit serves as a historical marker showing when and where branches were integrated.

Advantages:

  • Preserves complete branch history: The release branch's individual commits remain visible in git log --first-parent and tools like GitLab's merge request history. If a bug appears post-release, you can examine the exact sequence of commits that composed that release.
  • Clear merge points in history: The merge commit acts as a bookmark. Commands like git log --merges show only merge commits, providing a high-level view of when features/releases were integrated. This is useful for auditing and understanding project timeline.
  • Easier to track releases: Tags applied to merge commits clearly identify release boundaries. git describe can reference the merge commit, and git log v1.4.0..v1.5.0 shows exactly what changed between releases.

Command:

git merge --no-ff release/v1.5.0 -m "Release version 1.5.0"

This command merges the release branch into the current branch (typically main or develop) while forcing the creation of a merge commit. The -m flag provides the merge commit message, which serves as documentation in the git history. This message should be descriptive enough that future developers understand what release or hotfix was integrated at this point in the project timeline. The merge commit becomes a permanent marker in the repository's history that can be referenced when auditing releases or debugging production issues.

Fast-Forward Merge (Rarely Used)

Use for: Very small, single-commit changes when linear history is desired

When NOT to use:

  • Feature branches with multiple commits
  • When you want to preserve branch point in history

CI/CD Integration

Pipeline Stages by Branch Type

Branch PatternStagesDeploymentTrigger
feature/*, bugfix/*lint, test, build, securityNoneOn push + MR
developAll stages + integration testsDev environment (auto)On merge
release/*All stages + E2E testsStaging (auto)On push
mainAll stages + smoke testsProduction (manual approval)On merge
hotfix/*Fast-track all stagesProduction (expedited)On push

Automated Quality Gates

Quality gates act as automated gatekeepers in the CI/CD pipeline, preventing merges that don't meet minimum standards. Each gate runs as a separate pipeline job, and GitLab requires all jobs to pass (exit code 0) before allowing merge. If any gate fails, the merge button becomes disabled in the UI. This enforcement is configured via branch protection rules (see Branch Protection) which tie pipeline success to merge permissions. Gates run on every push to a merge request branch, with results visible in the MR UI.

Before merge to any protected branch:

Code Quality:

  • All tests pass (unit, integration, contract) - Pipeline fails on any test failure
  • Code coverage thresholds met - Tools like Istanbul/JaCoCo export coverage percentage; pipeline compares against minimum threshold (85%)
  • Mutation testing thresholds met - PITest/Stryker mutates code and verifies tests catch mutations; requires 80%+ mutation score
  • Linting passes (zero errors) - ESLint/Checkstyle exits with non-zero code if errors found
  • Code formatting correct - Prettier/Spotless checks formatting; fails if files would change

Security:

  • SAST scan clean
  • Dependency scan (no critical CVEs)
  • Secret detection (no leaked credentials)
  • Container scanning (if applicable)

Build:

  • Build succeeds without warnings
  • Docker image builds successfully (if applicable)
  • OpenAPI spec validation passes

Documentation:

  • MR description follows template
  • Jira ticket linked
  • All discussions resolved
  • Required approvals obtained

See: Pipeline Configuration Guide for CI/CD setup


Branch Lifecycle Timeline


Best Practices Summary

Do

  1. Keep feature branches short-lived (<1 week, ideally 2-5 days)
  2. Commit frequently with meaningful conventional commit messages
  3. Pull latest develop before creating feature branch
  4. Rebase feature branch on develop regularly to avoid conflicts
  5. Squash commits when merging features to develop
  6. Delete branches immediately after merge
  7. Tag all releases with semantic version numbers
  8. Document breaking changes in CHANGELOG and commit body
  9. Link commits to Jira tickets for traceability
  10. Run tests locally before pushing

Don't

  1. Commit directly to main or develop (always use MR)
  2. Let feature branches live >1 week
  3. Force push to shared branches (main, develop, release)
  4. Merge without code review (always require approval)
  5. Skip CI checks or override failures without justification
  6. Use generic commit messages ("fix", "update", "changes")
  7. Forget to update CHANGELOG for releases
  8. Mix multiple features in one branch
  9. Create branches from outdated develop (always pull first)
  10. Push sensitive data (secrets, credentials, keys)

Governance and Policies

Merge Request Requirements

For develop:

  • 1 approval from any developer
  • All CI/CD checks passing
  • No unresolved discussions
  • Conventional commit format
  • Linked to Jira ticket

For main (releases and hotfixes):

  • 2 approvals from maintainers/tech leads
  • All CI/CD checks passing
  • Code owner approval (if applicable)
  • Security team approval (for security-related changes)
  • Comprehensive testing evidence
  • Release notes / hotfix justification

Branch Deletion Policy

  • Feature branches: Auto-delete after merge (configured in GitLab)
  • Release branches: Manual deletion after successful deployment
  • Hotfix branches: Manual deletion after merge to both main and develop
  • Stale branches: Automatically cleaned after 30 days of inactivity

Historical Commit Policy

  • Never rewrite history on main or develop
  • Never force push to shared branches
  • Document all hotfixes in post-mortem
  • Maintain audit trail for compliance (all commits traceable to tickets)

Further Reading

Internal Documentation

External Resources


Summary

Key Takeaways:

  1. Two permanent branches: main (production) and develop (integration)
  2. Feature branches created from develop, merged back with squash
  3. Release branches for stabilization, merged to both main and develop
  4. Hotfix branches from main for critical production fixes
  5. Conventional commits enable clear history and automated changelogs
  6. Semantic versioning provides predictable release numbering
  7. Branch protection enforces code review and quality gates
  8. Code owners ensure domain expert approval
  9. Short-lived branches minimize merge conflicts
  10. Automated pipelines enforce quality before merge
  11. Audit trail maintained through Jira ticket links
  12. Never rewrite history on shared branches