Skip to content
ExponaBLOG
ENGINEERING CRAFT · DAY 043 / 100

Trunk-based development for Engineering Craft developers

Short-lived branches, frequent merges, fewer conflicts.

Expona Admin7 min read

Most teams I've worked with spend hours every week fighting merge conflicts. Long-lived branches create a false sense of safety—until integration day arrives and half your team is stuck untangling incompatible changes. Trunk-based development fixes this by keeping branches short-lived and main always ready to ship, turning a painful process into something boring and reliable.

What is trunk-based development

Trunk-based development is a branching strategy where developers work on short-lived branches (hours or a day or two) and merge frequently into a main branch (often called main or trunk). The key principle: main is always deployable. You don't hold large unfinished features on separate branches waiting for the perfect moment to integrate. Instead, you use feature flags to hide incomplete work, allowing continuous integration without constant production disruptions.

I started using this approach on a ride-hailing platform where we had seven engineers and release cycles every two weeks. Before trunk-based development, the final three days were chaos—merge conflicts that made no sense, features broken in ways we hadn't anticipated because they'd been isolated for too long. Switching to trunk-based development cut our integration time from days to hours.

Branches live for hours or a day or two

The hardest part of trunk-based development isn't technical—it's cultural. It requires discipline to keep branches small and merge often. A branch that lives for a week is already too long. A branch that lives for two weeks is a long-lived branch wearing a trunk-based development costume.

In practice, this means:

  • Each branch solves one problem: add a new API endpoint, fix a bug in the checkout flow, refactor a payment processor integration.
  • You commit to the branch frequently (multiple times a day). Small commits make code review faster and easier to revert if needed.
  • You open a pull request early, even if the work isn't finished. This gives your team visibility and allows async review to start while you keep working.
  • You merge within 24 hours. If you can't, your scope is too large.

On a travel SaaS I built, we had a rule: if your branch exists for more than one business day, you explain why to the team. Usually, the explanation forced us to break the work into smaller pieces. One engineer fought this initially—she said her feature was "too interconnected to split." Within a month, she became the strongest advocate for small branches because she realized how much faster she could move.

Small branches also mean smaller pull requests. A 300-line diff gets reviewed in 20 minutes. A 3,000-line diff sits in review for three days because reviewers dread diving in. The time saved on code review alone justifies the discipline of keeping branches short.

Feature flags hide unfinished work

Here's where trunk-based development breaks from older branching strategies: you can merge incomplete features into main without exposing them to users. Feature flags (also called feature toggles or feature switches) are conditional checks that hide functionality until it's ready.

A simple example from an e-commerce platform I worked on:

typescript

// Check if the new recommendation engine is enabled
if (featureFlags.isEnabled('new_recommendation_engine')) {
  const recommendations = await getAIRecommendations(userId);
  return recommendations;
} else {
  return getLegacyRecommendations(userId);
}

This pattern means your incomplete feature lives in main, but only reaches production if the flag is active. Your team can merge it Tuesday morning, test it in staging all week, and flip the flag to true Friday afternoon when you're confident. If something breaks, you flip it off without a rollback.

Feature flags come in different shapes:

  • Boolean flags: on or off. Simple, but limited.
  • Percentage-based rollout: enable for 5% of users, then 25%, then 100%. This catches issues in production before all your users see them.
  • User or cohort-based: enable only for beta testers, enterprise customers, or internal staff.
  • Contextual flags: enable based on time of day, region, or device type.

On the CRM platform I built, we used a flag service (LaunchDarkly, but you can build a simple Redis-backed system too) to control which customers saw new features. Sales could enable the new AI-powered lead scoring for a prospect at 2 PM, watch the metrics, and decide within an hour if it was working. No deployment pipeline needed.

The key constraint: don't let feature flags become a permanent band-aid. If a flag exists in production for more than a month, either commit to the feature (remove the flag and clean up the legacy code) or remove the feature entirely. Dead flags create technical debt.

Main is always deployable

In many teams, main is a gate-keeped branch. Only certain people merge there. Deployments happen on Friday if everyone's lucky, or Monday after a weekend of prayer. Under trunk-based development, main is boring—it's deployable every single day, sometimes multiple times a day.

This requires discipline in code quality:

  • Automated testing: you can't merge without a green test suite. Unit tests catch logic errors, integration tests verify the API contracts, end-to-end tests spot UI breakage.
  • Code review before merge: every change to main goes through at least one pair of eyes. I've caught production bugs in code review that would have cost hours of debugging later.
  • Linting and formatting: enforce style consistently so reviews focus on logic, not tabs versus spaces.
  • Type safety: on platforms using TypeScript or similar, type errors are caught before they reach production.

Here's what "main is deployable" actually means: right now, at any moment, you could take the current main branch, run your deployment pipeline, and have confidence it works. You might not want to deploy (a feature isn't finished yet, or you're waiting for the next release cycle). But you could without fear.

On a trading platform I worked on, deployability was non-negotiable. We needed to deploy hotfixes for market-moving bugs within minutes sometimes. We couldn't afford to wait for a release cycle. Main was always deployable, and that discipline saved us during a cascade of order routing failures one morning—we fixed it in 12 minutes instead of the "normal" 4 hours because main was in a known good state.

Painful merges disappear

The biggest win from trunk-based development: merges become boring. Boring is good.

In older workflows, long-lived branches create merge disasters. You have:

  • Two developers both rename a service but take different approaches. When they merge, who's right?
  • Three features touching the same configuration file. Line-by-line conflicts that are technically solvable but semantically wrong.
  • A database schema change on one branch, new ORM queries on another. Together, they fail at runtime even though both pass tests individually.

With trunk-based development, you merge daily into main. If there's a conflict, it's small—usually just a few lines. You resolve it immediately, verify the tests still pass, and move on. The person who wrote the code 20 minutes ago is still in context to fix it.

More importantly, you catch semantic conflicts early. If your branch adds a new required field to an API response, and another branch already merged that expects the old shape, you know about it within hours. You coordinate the fix that day instead of discovering it three weeks later when both features are "done" and neither is compatible with the other.

I've seen teams go from 2-3 serious merge conflicts per release cycle (each taking 2-4 hours to untangle) to essentially zero. That's not bragging rights—that's 6-12 hours per cycle of engineering time freed to actually build features.

Common mistakes

Letting branches live too long: "It's only been three days" is how five-day branches start. Set a hard limit (24 hours works for most teams) and break work into smaller pieces if you can't fit it in that window.

Over-using feature flags: flags are for hiding unfinished features, not for release management. If you're toggling a flag at deployment time every release, you're using them wrong. That's a different tool (blue-green deployments, canary releases).

Merging without tests passing: "I'll fix the tests later" means you don't trust your main branch. If you're not confident enough to deploy main right now, you're not doing trunk-based development.

Poor code review discipline: trunk-based development only works if you actually review code before merging. Skipping review to merge faster defeats the purpose. It's faster short-term and slower long-term (bugs get through).

Not cleaning up dead code: when a feature is enabled for everyone, remove the feature flag and the old code path. Otherwise, you end up with a codebase that's half-finished features hidden behind flags—that's legacy code.

Key takeaways

  • Keep branches alive for hours or a day or two, not weeks. Small scope means quick merges and fewer conflicts.
  • Use feature flags to hide incomplete work, allowing main to stay deployable while development continues.
  • Main should be deployment-ready at all times. This requires automated tests, code review, and discipline on quality gates.
  • Frequent merges into main catch semantic conflicts early, before features are "done" and locked into separate branches.
  • Trunk-based development trades upfront discipline for downstream speed. It's slower to learn but faster to ship.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles

Trunk-based development | Engineering Craft | Expona