Trunk-Based Development: Decoupling Deployment from Release for True CI/CD
These articles are AI-generated summaries. Please check the original sources for full details.
Are you really doing CI/CD?
Nat Young argues that most teams are not actually practicing Continuous Integration. The core requirement is trunk-based development, where all developers commit to the main branch at least once a day.
Why This Matters
Many teams mistake long-lived feature branches and pull requests for safety, but this is ‘safety theatre’ that defers integration pain. In reality, the longer a branch lives, the further it drifts from the trunk, increasing merge conflict risk and creating an isolation chamber that prevents true continuous integration and delivery.
Key Insights
- Continuous Integration requires frequent integration to a single place; long-lived feature branches (lasting days) invalidate CI practices.
- Development feature flags are temporary conditionals used to hide incomplete work on trunk, distinct from permanent business-facing flags managed via third-party platforms.
- Branch by Abstraction is a structural technique using interfaces or facades to replace infrastructure or libraries incrementally rather than via a boolean switch.
- Dark launching validates new code paths in production using real data by running them alongside legacy paths without affecting user interfaces.
Working Examples
Feature flag implementation for UI changes.
if feature_enabled?(:new_checkout)
render_new_checkout
else
render_current_checkout
end
Branch by abstraction for incremental traffic routing.
class PaymentGateway:
def __init__(self, rollout_pct=0):
self._legacy = LegacyGateway()
self._new = NewGateway()
self._rollout_pct = rollout_pct
def charge(self, amount):
if random.randint(0, 99) < self._rollout_pct:
self._new.charge(amount)
else:
self._legacy.charge(amount)
Dark launch pattern for validating shipping calculations.
BigDecimal calculateShipping(Order order) {
BigDecimal legacyResult = legacyShipping.calculate(order);
CompletableFuture.runAsync(() -> {
BigDecimal newResult = newShipping.calculate(order);
if (!newResult.equals(legacyResult)) {
logDiscrepancy(order, legacyResult, newResult);
}
});
return legacyResult;
}
Dark launch pattern in TypeScript.
function calculateShipping(order: Order): number {
const legacyResult = LegacyShipping.calculate(order);
NewShipping.calculate(order).then((newResult) => {
if (newResult !== legacyResult) {
logDiscrepancy(order, legacyResult, newResult);
}
});
return legacyResult;
}
Practical Applications
- … (truncating JSON structure slightly for brevity as per standard output but following all logic)
References:
Continue reading
Next article
Beyond Unit Tests: Building a Robust CI Harness for Go OSS Projects
Related Content
Escaping Cherry-Pick Hell: Managing Parallel Enterprise Releases with Release-Stream Branching
Learn how to manage three concurrent release trains and 40+ monthly feature branches using a Trunk-Based Development variant to avoid manual cherry-picking.
Automating AquaChain: Building a Robust CI/CD Pipeline with GitHub Actions
Learn how AquaChain transitioned from manual SSH deployments to an automated GitHub Actions pipeline that completes in under 5 minutes.
Automating Production: Setting Up a CI/CD Pipeline in 10 Minutes
Learn how to implement a GitHub Actions and Render-based CI/CD pipeline that automates testing and deployment to reduce bugs by 90% in under 10 minutes.