Dark Code
Deploy new logic to production before anything calls it, so it carries zero release risk until you wire it in.
4 minute read
Phase 1 - Foundations | Scope: Team
Trunk-based development requires integrating incomplete work daily without breaking trunk or exposing half-built features to users. This section covers the techniques that make that possible, ordered from least to most costly to maintain.
Deployment is a technical action: pushing code to production. Release is a business decision: making a capability available to users. Evolutionary coding techniques are how you deploy continuously while controlling release independently.
Feature flags are the best-known way to make that separation, which is why teams reach for them first. But a runtime if (flag) branch is conditional complexity: every flag combination has to be tested, and the flag has to be deleted later or it becomes permanent debt. Most incomplete work does not need a flag at all. It needs to be structured so the unfinished parts are inert until they are ready.
Use the least intrusive technique that solves the problem. Reach for a flag only when nothing simpler applies.
| Page | What You’ll Learn |
|---|---|
| Dark Code | Deploy new logic before anything calls it, so it carries zero release risk |
| Branch by Abstraction | Replace an existing implementation behind a stable interface, one commit at a time |
| Parallel Run | Prove a new implementation matches production behavior before you trust it |
| Expand and Contract | Evolve a shared database schema or API contract without a breaking change |
Work down this list. Each technique carries more long-term maintenance cost than the one above it.
| Technique | What It Costs to Maintain |
|---|---|
| Dark code | Nothing. The code sits unreferenced until it’s wired in. |
| Branch by abstraction | One interface and one deletion once the swap is complete. |
| Parallel run | A temporary comparison harness. |
| Expand and contract | A multi-step migration with a defined end state. |
| Strangler fig | A routing layer, but no branching inside the code it replaces. |
| Feature flags | Ongoing lifecycle management: an owner, a removal date, and combinatorial test cases until it’s deleted. |
Ask these questions in order. Stop at the first “yes.”
Reaching question 6 is a legitimate reason to flag. Reaching for a flag at question 1 is not.
See Feature Flags for the full flag lifecycle, from creation through removal.
Familiarity is not a reason to skip the hierarchy. A flag left in place after launch is technical debt that dark code or branch by abstraction would never have created in the first place.
Extract the interface first, as its own zero-behavior-change commit, before starting the swap. Introducing an abstraction and a new implementation in the same change makes the abstraction hard to review on its own merits.
A shared schema or API is a contract other people’s code depends on. Use expand and contract rather than branch by abstraction for anything consumed outside your own codebase.
These techniques are what make trunk-based development safe for incomplete work. Once your team can integrate daily without flag sprawl, continue building the test architecture that backs it.
Deploy new logic to production before anything calls it, so it carries zero release risk until you wire it in.
Replace an existing implementation behind a stable interface, one small commit at a time, without a long-lived branch.
Run a new implementation alongside the old one in production and compare results before you trust it.
Evolve a shared database schema or API contract across non-breaking phases instead of mutating it in place.