Everything is a Feature Toggle

Image © 20th Century Fox

Macklin Hartley

@macklin_hartley

macklin.me

Image © 20th Century Fox

Image © 20th Century Fox

Everything is a Feature Toggle

Image © 20th Century Fox

An Abridged History

What is a Feature Toggle?

Techniques and Strategy

Outline

  1. Checkout
  2. Implement Feature
  3. Build
  4. Test Feature
  5. Sync (update / pull)
  6. Merge
  7. Build
  8. Test
  9. Smoke Tests
  10. Check In (commit / push)
  11. Daily Build

Trunk Based Development

  1. Checkout
  2. Implement Feature
  3. Build
  4. Test Feature
  5. Sync (update / pull)
  6. Merge
  7. Build
  8. Test
  9. Smoke Tests
  10. Check In (commit / push)
  11. Daily Build

Trunk Based Development

1985 - 2005

✅ Microsoft Secrets

✅ ThoughtWorks

✅ Continuous Integration

Trunk Based Development

⛔️ Distributed Version Control

⛔️ Lightweight Branching

⛔️ Pull Requests

+

2005 - 2010

1985 - 2005

✅ Microsoft Secrets

✅ ThoughtWorks

✅ Continuous Integration

Trunk Based Development

+

2005 - 2010

⛔️ Distributed Version Control

⛔️ Lightweight Branching

⛔️ Pull Requests

Trunk Based Development

✅ DevOps Movement

✅ Continuous Delivery

Feature Toggles

+

2005 - 2010

2010 - Present

⛔️ Distributed Version Control

⛔️ Lightweight Branching

⛔️ Pull Requests

💥

💥

💥

Image by generated.photos

Cones of Change

Feature

Small Changes

Continuous Integration

Continuous Integration

Image by generated.photos

Continuous Integration

Trunk

Image by generated.photos

Trunk Based Development

Process Feature Branches Trunk-based
Branches Long-lived Short-lived
Batch Size N Features < 1 Feature
Change Risk High Low

Trunk Based Development

Process Feature Branches Trunk-based
Branches Long-lived Short-lived
Batch Size N Features < 1 Feature
Change Risk High Low

Trunk Based Development

Process Feature Branches Trunk-based
Branches Long-lived Short-lived
Batch Size N Features < 1 Feature
Change Risk High Low

A branching model for code collaboration on a single branch, enabling Continuous Integration

Trunk Based Development

An Abridged History

What is a Feature Toggle?

Techniques and Strategy

A software engineering technique for enabling feature lifecycle management
with or without deploying new code.

What is a Feature Toggle?

What is a Feature Toggle?

interface FeatureToggle {
  (feature: string): boolean
}

((isEnabled: FeatureToggle) => {
  if (isEnabled('feature')) {
    log('Feature is enabled! 🎉')
  } else {
    log('Feature is disabled! ☹️')
  }
})

What Happens?

interface FeatureToggle {
  (feature: string): boolean
}

((isEnabled: FeatureToggle) => {
  if (isEnabled('feature')) {
    log('Feature is enabled! 🎉')
  } else {
    log('Feature is disabled! ☹️')
  }
})

🤔

Persistence

PostgreSQL

MySQL

MariaDB

Azure SQL

DynamoDB

MongoDB

CouchDB

Cosmos DB

interface FeatureToggle {
  (feature: string): boolean
}

Persistence

JSON

YAML

CSV

Exotic Format

LaunchDarkly

Optimizely

Calculated

Code

interface FeatureToggle {
  (feature: string): boolean
}

Evaluation

Per-Request

Startup

Build-time / Compile-time

Static

interface FeatureToggle {
  (feature: string): boolean
}

Longevity

Hours

Weeks

Months

Years

interface FeatureToggle {
  (feature: string): boolean
}

Behaviour

Default / Fallback

Consistent Output

Throws Exceptions

Input Validation

interface FeatureToggle {
  (feature: string): boolean
}

Hypothesis

interface FeatureToggle {
  (feature: string): boolean
}

const alwaysOff: FeatureToggle = () => false

((isEnabled: FeatureToggle) => {
  if (isEnabled('feature')) {
    log('Feature is enabled! 🎉')
  } else {
    log('Feature is disabled! ☹️')
  }
})(alwaysOff)

Is it a Feature Toggle?

interface FeatureToggle {
  (feature: string): boolean
}

const alwaysOff: FeatureToggle = () => false

((isEnabled: FeatureToggle) => {
  if (isEnabled('feature')) {
    log('Feature is enabled! 🎉')
  } else {
    log('Feature is disabled! ☹️')
  }
})(alwaysOff)
// Redundant code removed

(() => {
  if (false) {
    log('Feature is enabled! 🎉')
  } else {
    log('Feature is disabled! ☹️')
  }
})()

How about now?

// Redundant code removed

(() => {
  log('Feature is disabled! ☹️')
})()

Hold up... Really?

Static Toggle

Persisted in Code

Consistent Static Evaluation

Branch by Abstraction

const alwaysOff: FeatureToggle = () => false
const alwaysOn: FeatureToggle = () => true

{ old }

Branch by Abstraction

{ old }

Static Toggle

Branch by Abstraction

false

true

{ old }

{ new }

Branch by Abstraction

Static Toggle

false

true

{ old }

{ new }

Branch by Abstraction

Static Toggle

false

true

{ new }

Branch by Abstraction

Feature toggles allow incomplete and un-tested code paths to be shipped to production as Latent Code, which may never be turned on.

Static Toggle

const alwaysOff: FeatureToggle = () => false
const alwaysOn: FeatureToggle = () => true

const useOldPaymentGateway = () => {
  // Old implementation.
}

const useNewPaymentGateway = () => {
  // New implementation.
}

const pay = (isEnabled = alwaysOff): boolean =>
  isEnabled('new-payment-gateway')
    ? useNewPaymentGateway()
    : useOldPaymentGateway()

Static Toggle

const alwaysOff: FeatureToggle = () => false
const alwaysOn: FeatureToggle = () => true

const runTests = isEnabled => {
  it('makes payment successfully', 
    expect(pay(isEnabled)).toEqual(true))
}

describe('old-gateway', () => runTests(alwaysOff))
describe('new-gateway', () => runTests(alwaysOn))

Longevity

Evaluation

Static

Days

Weeks

Months

Static

Build-time

Per-Request

Runtime Configuration

Release

Cost

Environment Toggle

Persisted in Code

Evaluated at Build-time or Start-up

Integration or UAT Testing

const environmentToggle: FeatureToggle => 
  feature => 
    process.env.NODE_ENV === 'development'
      ? devToggle(feature) : alwaysOff()

Environment Toggle

const devToggle: FeatureToggle = feature => {
  switch (feature) {
    case 'new-payment-gateway': return true
    default: return false
  }
}

const alwaysOff: FeatureToggle = () => false

const environmentToggle: FeatureToggle => 
  feature => 
    process.env.NODE_ENV === 'development'
      ? devToggle(feature) : alwaysOff()

Longevity

Evaluation

Static

Days

Weeks

Months

Environment

Static

Build-time

Per-Request

Runtime Configuration

Release

Cost

Application Toggle

Persistence varies

Evaluate at startup or per-request

Versatile and extendable

const databaseToggle: FeatureToggle => feature =>
  pool.query(
   `SELECT enabled FROM toggle WHERE feature = $1`,
    [feature],
  ).then(({ rows }) => rows[0]?.enabled ?? false)

Canary Toggle

const canaryToggle: UserToggle => 
  (feature, userId) => pool.query(
   `SELECT 1 FROM canary 
    WHERE feature = $1 AND user_id = $2`,
    [feature, userId],
  ).then(({ rows }) => !!rows[0])

Toggle based on indentifiers

Evaluate per-request

Used for canary releases

Calculated Toggle

Calculation of per-user attributes

Evaluate per-request

Used in A/B Testing

// Deterministically output between 1 and 0.
const hash = (input: string): number => { ... };

const calculatedToggle: UserToggle => 
  (feature, id) => hash(`${feature}${id}`) > 0.5

Longevity

Evaluation

Static

Days

Weeks

Months

Environment

Static

Build-time

Per-Request

Runtime Configuration

Release

Experiment

Application

Canary

Calculated

Cost

Circuit Breaker

const circuitBreaker = (event: Event): boolean =>
  queue.length < 100 ? !!queue.push(event) : false

Calculation of system health

Evaluate per-request

Used for operational purposes

Kill Switch

const killSwitch = (name: string) => 
  (req, res, next) => {
    if (!feature.isEnabled(name)) {
      throw new Error('Endpoint is disabled')
    }
    next()
  }

Manually-managed circuit breaker

Longevity

Evaluation

Static

Days

Weeks

Months

Environment

Static

Build-time

Per-Request

Runtime Configuration

Release

Operational

Experiment

Application

Canary

Calculated

Kill switch

Circuit Breaker

Cost

Customization

Longevity

Evaluation

Static

Days

Weeks

Months

Environment

Static

Build-time

Per-Request

Runtime Configuration

Release

Operational

Experiment

Application

Canary

Calculated

Customization

Permission

Kill switch

Circuit Breaker

An Abridged History

What is Feature Toggles?

Techniques and Strategy

Branch by Abstraction

Feature Branches Trunk-based

Source Control
 

Code Execution
 

Linear

Linear

Branching

Branching

Branch by Abstraction

Feature Branches Trunk-based

Source Control
 

Code Execution
 

Linear

Linear

Branching

Branching

Long-lived Feature Branches

Already merged

Cannot be merged

No longer relevant

Branching

Long-lived Feature Toggles

Permanently ON

Permanently OFF

Is business logic

Branching

Removing Toggles

Every toggle should have an expiry

it('toggle expires after 1 month', () => 
  expect(+new Date())
    .toBeLessThan(Date.parse('2021-09-14')))

Removing Toggles

Remove expired toggles proactively

it('toggle expires after 1 month', () => 
  expect(+new Date())
    .toBeLessThan(Date.parse('2021-09-14')))

Naming Toggles

🤔

const pay = () => isEnabled('internal-staff')
  ? useOldPaymentGateway : useNewPaymentGateway
const sendEmail = () => isEnabled('is-production')
  ? actuallySendEmail : mockSendEmail
const getAlgorithm () => isEnabled('is-production')
  ? oldAlgorithm : fancyNewAlgorithm

Naming Toggles

const sendEmail = () => isEnabled('internal-staff')
  ? actuallySendEmail : mockSendEmail
const calculate = () => isEnabled('internal-staff')
  ? oldAlgorithm : fancyNewAlgorithm

Naming - Be specific

const pay = () => isEnabled('internal-staff')
  ? useOldPaymentGateway : useNewPaymentGateway

Naming Toggles

const sendEmail = () => isEnabled('enable-smtp')
  ? actuallySendEmail : mockSendEmail
const calculate = () => isEnabled('use-old-algo')
  ? oldAlgorithm : fancyNewAlgorithm

Naming - Be specific

const pay = () => isEnabled('use-old-gateway')
  ? useOldPaymentGateway : useNewPaymentGateway

Naming Toggles

const pay = () => isEnabled('PAY-123')
  ? useOldPaymentGateway : useNewPaymentGateway

Naming - Avoid indirection

Naming Toggles

const pay = () => isEnabled('reticulate-splines')
  ? useOldPaymentGateway : useNewPaymentGateway

Naming - Don't be cutesy

Naming Toggles

const pay = () => isEnabled('project-orange')
  ? useOldPaymentGateway : useNewPaymentGateway

Naming - Don't be cutesy

Use an Audit Log

Longevity

Evaluation

Static

Days

Weeks

Months

Environment

Static

Build-time

Per-Request

Runtime Configuration

Application

Canary

Calculated

Kill switch

Circuit Breaker

Use an Audit Log

Customization

Longevity

Evaluation

Static

Days

Weeks

Months

Environment

Static

Build-time

Per-Request

Runtime Configuration

Application

Canary

Calculated

Kill switch

Circuit Breaker

Source Control

Use an Audit Log

Customization

Longevity

Evaluation

Static

Days

Weeks

Months

Environment

Static

Build-time

Per-Request

Runtime Configuration

Application

Canary

Calculated

Kill switch

Circuit Breaker

Change Log

Use an Audit Log

Customization

Longevity

Evaluation

Static

Days

Weeks

Months

Environment

Static

Build-time

Per-Request

Runtime Configuration

Application

Canary

Calculated

Product

Kill switch

Circuit Breaker

Decision Log

Lessons

Remove toggles pro-actively

Use deliberate and descriptive naming

Use an Audit Log

Image © 20th Century Fox

Image © 20th Century Fox

Macklin Hartley

@macklin_hartley

macklin.me