Image © 20th Century Fox
Image © 20th Century Fox
Image © 20th Century Fox
Image © Doncho Angelov https://www.flickr.com/photos/donangel/440634396
Image © Doncho Angelov https://www.flickr.com/photos/donangel/440634396
Image © 20th Century Fox
An Abridged History
What is a Feature Toggle?
Techniques and Strategy
1985 - 2005
✅ Microsoft Secrets
✅ ThoughtWorks
✅ Continuous Integration
⛔️ Distributed Version Control
⛔️ Lightweight Branching
⛔️ Pull Requests
+
2005 - 2010
1985 - 2005
✅ Microsoft Secrets
✅ ThoughtWorks
✅ Continuous Integration
+
2005 - 2010
⛔️ Distributed Version Control
⛔️ Lightweight Branching
⛔️ Pull Requests
✅ DevOps Movement
✅ Continuous Delivery
✅ Feature Toggles
+
2005 - 2010
2010 - Present
⛔️ Distributed Version Control
⛔️ Lightweight Branching
⛔️ Pull Requests
💥
💥
💥
Image by generated.photos
Feature
Small Changes
Image by generated.photos
Trunk
Image by generated.photos
Process | Feature Branches | Trunk-based |
---|---|---|
Branches | Long-lived | Short-lived |
Batch Size | N Features | < 1 Feature |
Change Risk | High | Low |
Process | Feature Branches | Trunk-based |
---|---|---|
Branches | Long-lived | Short-lived |
Batch Size | N Features | < 1 Feature |
Change Risk | High | Low |
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
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.
interface FeatureToggle {
(feature: string): boolean
}
((isEnabled: FeatureToggle) => {
if (isEnabled('feature')) {
log('Feature is enabled! 🎉')
} else {
log('Feature is disabled! ☹️')
}
})
interface FeatureToggle {
(feature: string): boolean
}
((isEnabled: FeatureToggle) => {
if (isEnabled('feature')) {
log('Feature is enabled! 🎉')
} else {
log('Feature is disabled! ☹️')
}
})
🤔
PostgreSQL
MySQL
MariaDB
Azure SQL
DynamoDB
MongoDB
CouchDB
Cosmos DB
interface FeatureToggle {
(feature: string): boolean
}
JSON
YAML
CSV
Exotic Format
LaunchDarkly
Optimizely
Calculated
Code
interface FeatureToggle {
(feature: string): boolean
}
Per-Request
Startup
Build-time / Compile-time
Static
interface FeatureToggle {
(feature: string): boolean
}
Hours
Weeks
Months
Years
interface FeatureToggle {
(feature: string): boolean
}
Default / Fallback
Consistent Output
Throws Exceptions
Input Validation
interface FeatureToggle {
(feature: string): boolean
}
interface FeatureToggle {
(feature: string): boolean
}
const alwaysOff: FeatureToggle = () => false
((isEnabled: FeatureToggle) => {
if (isEnabled('feature')) {
log('Feature is enabled! 🎉')
} else {
log('Feature is disabled! ☹️')
}
})(alwaysOff)
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! ☹️')
}
})()
// Redundant code removed
(() => {
log('Feature is disabled! ☹️')
})()
Persisted in Code
Consistent Static Evaluation
Branch by Abstraction
const alwaysOff: FeatureToggle = () => false
const alwaysOn: FeatureToggle = () => true
{ old }
{ old }
❌
Static Toggle
false
true
{ old }
{ new }
Static Toggle
false
true
{ old }
{ new }
Static Toggle
false
true
{ new }
Feature toggles allow incomplete and un-tested code paths to be shipped to production as Latent Code, which may never be turned on.
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()
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
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()
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
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)
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
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
const circuitBreaker = (event: Event): boolean =>
queue.length < 100 ? !!queue.push(event) : false
Calculation of system health
Evaluate per-request
Used for operational purposes
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
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
Feature Branches | Trunk-based | |
---|---|---|
Source Control |
||
Code Execution |
Linear
Linear
Branching
Branching
Feature Branches | Trunk-based | |
---|---|---|
Source Control |
||
Code Execution |
Linear
Linear
Branching
Branching
Already merged
Cannot be merged
No longer relevant
Branching
Permanently ON
Permanently OFF
Is business logic
Branching
Every toggle should have an expiry
it('toggle expires after 1 month', () =>
expect(+new Date())
.toBeLessThan(Date.parse('2021-09-14')))
❌
Remove expired toggles proactively
it('toggle expires after 1 month', () =>
expect(+new Date())
.toBeLessThan(Date.parse('2021-09-14')))
🤔
const pay = () => isEnabled('internal-staff')
? useOldPaymentGateway : useNewPaymentGateway
const sendEmail = () => isEnabled('is-production')
? actuallySendEmail : mockSendEmail
const getAlgorithm () => isEnabled('is-production')
? oldAlgorithm : fancyNewAlgorithm
const sendEmail = () => isEnabled('internal-staff')
? actuallySendEmail : mockSendEmail
const calculate = () => isEnabled('internal-staff')
? oldAlgorithm : fancyNewAlgorithm
Naming - Be specific
const pay = () => isEnabled('internal-staff')
? useOldPaymentGateway : useNewPaymentGateway
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
const pay = () => isEnabled('PAY-123')
? useOldPaymentGateway : useNewPaymentGateway
Naming - Avoid indirection
const pay = () => isEnabled('reticulate-splines')
? useOldPaymentGateway : useNewPaymentGateway
Naming - Don't be cutesy
const pay = () => isEnabled('project-orange')
? useOldPaymentGateway : useNewPaymentGateway
Naming - Don't be cutesy
Longevity
Evaluation
Static
Days
Weeks
Months
Environment
Static
Build-time
Per-Request
Runtime Configuration
Application
Canary
Calculated
Kill switch
Circuit Breaker
Customization
Longevity
Evaluation
Static
Days
Weeks
Months
Environment
Static
Build-time
Per-Request
Runtime Configuration
Application
Canary
Calculated
Kill switch
Circuit Breaker
Source Control
Customization
Longevity
Evaluation
Static
Days
Weeks
Months
Environment
Static
Build-time
Per-Request
Runtime Configuration
Application
Canary
Calculated
Kill switch
Circuit Breaker
Change 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
Remove toggles pro-actively
Use deliberate and descriptive naming
Use an Audit Log
Image © 20th Century Fox
Image © 20th Century Fox