Evolutionary Coding Techniques

Choose the least intrusive technique for integrating incomplete work to trunk, from dark code to feature flags as a last resort.

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 Not Release

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.

In This Section

PageWhat You’ll Learn
Dark CodeDeploy new logic before anything calls it, so it carries zero release risk
Branch by AbstractionReplace an existing implementation behind a stable interface, one commit at a time
Parallel RunProve a new implementation matches production behavior before you trust it
Expand and ContractEvolve a shared database schema or API contract without a breaking change

The Hierarchy of Techniques

Work down this list. Each technique carries more long-term maintenance cost than the one above it.

TechniqueWhat It Costs to Maintain
Dark codeNothing. The code sits unreferenced until it’s wired in.
Branch by abstractionOne interface and one deletion once the swap is complete.
Parallel runA temporary comparison harness.
Expand and contractA multi-step migration with a defined end state.
Strangler figA routing layer, but no branching inside the code it replaces.
Feature flagsOngoing lifecycle management: an owner, a removal date, and combinatorial test cases until it’s deleted.

How to Choose

Ask these questions in order. Stop at the first “yes.”

  1. Can the change be introduced additively, with nothing pointing to it yet? Use dark code.
  2. Does an existing interface already isolate this behavior, or can you extract one first? Use branch by abstraction.
  3. Do you need to prove the new logic produces the same answer as the old logic before you trust it? Use parallel run.
  4. Are you changing a shared contract, like a database schema or an API, that other code or services depend on? Use expand and contract.
  5. Are you replacing a whole subsystem or service, not a single implementation? Use the strangler fig pattern at the routing layer.
  6. Is this strictly a business release timing decision, such as a coordinated launch, a kill switch, an entitlement, or an experiment, that none of the above can express? Use a feature flag, and only at the edge of the system.

Reaching question 6 is a legitimate reason to flag. Reaching for a flag at question 1 is not.

Rules If You Do Reach for a Flag

  • Flag at the edge, not in domain logic. Put the check in a controller, router, or top-level entry point, never buried inside business logic or a data-access layer.
  • Give every flag an expiration date at creation time. No date means the flag is permanent, and permanent release flags are debt.
  • Create the removal ticket in the same pull request that adds the flag. Not later.
  • Never nest flags. If capability B depends on capability A, extend A’s flag instead of stacking a second flag on top of it.

See Feature Flags for the full flag lifecycle, from creation through removal.

Key Pitfalls

1. “We used a flag because it was the tool we already knew”

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.

2. “We picked branch by abstraction, but nothing was calling the old code through an interface yet”

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.

3. “We treated a database migration like a code refactor”

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.

Next Step

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.


Dark Code

Deploy new logic to production before anything calls it, so it carries zero release risk until you wire it in.

Branch by Abstraction

Replace an existing implementation behind a stable interface, one small commit at a time, without a long-lived branch.

Parallel Run

Run a new implementation alongside the old one in production and compare results before you trust it.

Expand and Contract

Evolve a shared database schema or API contract across non-breaking phases instead of mutating it in place.