Feature Flags System

Exercise: Feature Flags System

Difficulty - Intermediate

Learning Objectives

  • Implement a dynamic feature flag management system
  • Build rule-based evaluation engines
  • Support percentage-based rollouts
  • Implement user targeting and segmentation
  • Create thread-safe flag updates

Problem Statement

Create a comprehensive feature flag system that allows dynamic feature toggling in production without deployments. Your implementation should support various evaluation rules, percentage-based rollouts, user targeting, and real-time flag updates.

Requirements

1. Basic Feature Flag Manager

Implement a flag manager that:

  • Stores feature flags with enabled/disabled state
  • Supports adding, updating, and removing flags
  • Is thread-safe for concurrent access
  • Returns default values when flags don't exist
  • Provides IsEnabled(flagName, context) method

Example Usage:

 1manager := NewFeatureFlagManager()
 2manager.AddFlag(&Flag{
 3    Name:    "new-checkout",
 4    Enabled: true,
 5})
 6
 7if manager.IsEnabled("new-checkout", userContext) {
 8    // Use new checkout flow
 9} else {
10    // Use old checkout flow
11}

2. Percentage-Based Rollout

Create a percentage rollout rule that:

  • Enables feature for N% of users
  • Uses consistent hashing based on user ID
  • Ensures same user always gets same result
  • Supports gradual rollout
  • Distributes users evenly across the percentage

Example Usage:

 1flag := &Flag{
 2    Name:    "new-ui",
 3    Enabled: true,
 4    Rules: []Rule{
 5        &PercentageRule{Percentage: 25}, // 25% of users
 6    },
 7}
 8
 9manager.AddFlag(flag)
10
11// User "user123" will consistently see the feature
12ctx := map[string]interface{}{"user_id": "user123"}
13enabled := manager.IsEnabled("new-ui", ctx)

3. User Targeting Rules

Implement user targeting that:

  • Targets specific user IDs or groups
  • Supports whitelist and blacklist patterns
  • Allows email domain matching
  • Enables attribute-based targeting
  • Combines multiple targeting rules with AND/OR logic

Example Usage:

 1flag := &Flag{
 2    Name:    "premium-feature",
 3    Enabled: true,
 4    Rules: []Rule{
 5        &UserTargetingRule{
 6            TargetUsers: []string{"user1", "user2"},
 7        },
 8        &AttributeRule{
 9            Attribute: "plan",
10            Operator:  Equals,
11            Value:     "premium",
12        },
13    },
14}
15
16ctx := map[string]interface{}{
17    "user_id": "user1",
18    "plan":    "premium",
19}
20enabled := manager.IsEnabled("premium-feature", ctx)

4. Time-Based Rules

Create time-based flag evaluation that:

  • Enables features during specific time windows
  • Supports scheduled feature launches
  • Handles timezone-aware time ranges
  • Allows day-of-week and time-of-day restrictions
  • Supports countdown-based enabling

Example Usage:

 1flag := &Flag{
 2    Name:    "black-friday-sale",
 3    Enabled: true,
 4    Rules: []Rule{
 5        &TimeWindowRule{
 6            Start: time.Date(2024, 11, 29, 0, 0, 0, 0, time.UTC),
 7            End:   time.Date(2024, 12, 2, 23, 59, 59, 0, time.UTC),
 8        },
 9    },
10}
11
12// Only enabled during Black Friday weekend
13enabled := manager.IsEnabled("black-friday-sale", context)

5. Composite Rules and Logic

Support complex rule combinations:

  • AND logic: All rules must pass
  • OR logic: Any rule must pass
  • NOT logic: Inverts rule result
  • Nested rule groups for complex conditions
  • Short-circuit evaluation for performance

Example Usage:

 1flag := &Flag{
 2    Name:    "beta-feature",
 3    Enabled: true,
 4    Rules: []Rule{
 5        &AndRule{
 6            Rules: []Rule{
 7                &PercentageRule{Percentage: 10},
 8                &AttributeRule{
 9                    Attribute: "beta_tester",
10                    Operator:  Equals,
11                    Value:     true,
12                },
13                &NotRule{
14                    Rule: &UserTargetingRule{
15                        ExcludeUsers: []string{"blocked-user"},
16                    },
17                },
18            },
19        },
20    },
21}

Function Signatures

  1package featureflags
  2
  3import (
  4    "sync"
  5    "time"
  6)
  7
  8// Flag represents a feature flag with evaluation rules
  9type Flag struct {
 10    Name        string
 11    Enabled     bool
 12    Description string
 13    Rules       []Rule
 14    DefaultValue bool
 15}
 16
 17// Rule defines the interface for flag evaluation rules
 18type Rule interface {
 19    Evaluate(context map[string]interface{}) bool
 20}
 21
 22// FeatureFlagManager manages feature flags
 23type FeatureFlagManager struct {
 24    mu    sync.RWMutex
 25    flags map[string]*Flag
 26}
 27
 28// NewFeatureFlagManager creates a new feature flag manager
 29func NewFeatureFlagManager() *FeatureFlagManager
 30
 31// AddFlag adds or updates a feature flag
 32func AddFlag(flag *Flag)
 33
 34// RemoveFlag removes a feature flag
 35func RemoveFlag(name string)
 36
 37// IsEnabled checks if a feature is enabled for the given context
 38func IsEnabled(name string, context map[string]interface{}) bool
 39
 40// GetFlag retrieves a flag by name
 41func GetFlag(name string)
 42
 43// PercentageRule enables feature for percentage of users
 44type PercentageRule struct {
 45    Percentage int // 0-100
 46}
 47
 48func Evaluate(ctx map[string]interface{}) bool
 49
 50// UserTargetingRule targets specific users
 51type UserTargetingRule struct {
 52    TargetUsers  []string
 53    ExcludeUsers []string
 54}
 55
 56func Evaluate(ctx map[string]interface{}) bool
 57
 58// AttributeRule evaluates based on context attributes
 59type AttributeRule struct {
 60    Attribute string
 61    Operator  Operator
 62    Value     interface{}
 63}
 64
 65type Operator int
 66
 67const (
 68    Equals Operator = iota
 69    NotEquals
 70    GreaterThan
 71    LessThan
 72    Contains
 73)
 74
 75func Evaluate(ctx map[string]interface{}) bool
 76
 77// TimeWindowRule enables feature during specific time window
 78type TimeWindowRule struct {
 79    Start time.Time
 80    End   time.Time
 81}
 82
 83func Evaluate(ctx map[string]interface{}) bool
 84
 85// AndRule requires all rules to pass
 86type AndRule struct {
 87    Rules []Rule
 88}
 89
 90func Evaluate(ctx map[string]interface{}) bool
 91
 92// OrRule requires any rule to pass
 93type OrRule struct {
 94    Rules []Rule
 95}
 96
 97func Evaluate(ctx map[string]interface{}) bool
 98
 99// NotRule inverts the rule result
100type NotRule struct {
101    Rule Rule
102}
103
104func Evaluate(ctx map[string]interface{}) bool

Test Cases

Your implementation should pass these test scenarios:

  1// Test basic flag enabled/disabled
  2func TestBasicFlagEvaluation() {
  3    manager := NewFeatureFlagManager()
  4
  5    manager.AddFlag(&Flag{
  6        Name:    "test-flag",
  7        Enabled: true,
  8    })
  9
 10    assert.True(t, manager.IsEnabled("test-flag", nil))
 11
 12    manager.AddFlag(&Flag{
 13        Name:    "test-flag",
 14        Enabled: false,
 15    })
 16
 17    assert.False(t, manager.IsEnabled("test-flag", nil))
 18}
 19
 20// Test percentage rule distribution
 21func TestPercentageRuleDistribution() {
 22    manager := NewFeatureFlagManager()
 23
 24    manager.AddFlag(&Flag{
 25        Name:    "rollout-flag",
 26        Enabled: true,
 27        Rules:   []Rule{&PercentageRule{Percentage: 50}},
 28    })
 29
 30    enabled := 0
 31    for i := 0; i < 1000; i++ {
 32        ctx := map[string]interface{}{
 33            "user_id": fmt.Sprintf("user%d", i),
 34        }
 35        if manager.IsEnabled("rollout-flag", ctx) {
 36            enabled++
 37        }
 38    }
 39
 40    // Should be approximately 50%
 41    assert.InDelta(t, 500, enabled, 100)
 42}
 43
 44// Test user targeting
 45func TestUserTargeting() {
 46    manager := NewFeatureFlagManager()
 47
 48    manager.AddFlag(&Flag{
 49        Name:    "vip-feature",
 50        Enabled: true,
 51        Rules: []Rule{
 52            &UserTargetingRule{
 53                TargetUsers: []string{"vip1", "vip2"},
 54            },
 55        },
 56    })
 57
 58    ctx1 := map[string]interface{}{"user_id": "vip1"}
 59    assert.True(t, manager.IsEnabled("vip-feature", ctx1))
 60
 61    ctx2 := map[string]interface{}{"user_id": "regular"}
 62    assert.False(t, manager.IsEnabled("vip-feature", ctx2))
 63}
 64
 65// Test time window rule
 66func TestTimeWindowRule() {
 67    now := time.Now()
 68    future := now.Add(1 * time.Hour)
 69    past := now.Add(-1 * time.Hour)
 70
 71    rule := &TimeWindowRule{
 72        Start: past,
 73        End:   future,
 74    }
 75
 76    assert.True(t, rule.Evaluate(nil)) // Now is within window
 77
 78    rule = &TimeWindowRule{
 79        Start: future,
 80        End:   future.Add(1 * time.Hour),
 81    }
 82
 83    assert.False(t, rule.Evaluate(nil)) // Now is before window
 84}
 85
 86// Test composite AND rule
 87func TestAndRule() {
 88    manager := NewFeatureFlagManager()
 89
 90    manager.AddFlag(&Flag{
 91        Name:    "complex-flag",
 92        Enabled: true,
 93        Rules: []Rule{
 94            &AndRule{
 95                Rules: []Rule{
 96                    &PercentageRule{Percentage: 100},
 97                    &AttributeRule{
 98                        Attribute: "country",
 99                        Operator:  Equals,
100                        Value:     "US",
101                    },
102                },
103            },
104        },
105    })
106
107    ctx := map[string]interface{}{
108        "user_id": "user1",
109        "country": "US",
110    }
111    assert.True(t, manager.IsEnabled("complex-flag", ctx))
112
113    ctx["country"] = "UK"
114    assert.False(t, manager.IsEnabled("complex-flag", ctx))
115}

Common Pitfalls

⚠️ Watch out for these common mistakes:

  1. Inconsistent hashing: Same user should always get same percentage result
  2. Race conditions: Flag updates must be thread-safe
  3. Missing context: Always handle nil/missing context values gracefully
  4. Type assertions: Context values can be any type - validate before using
  5. Rule evaluation order: AND rules should short-circuit on first false
  6. Default values: Return sensible defaults when flags don't exist

Hints

💡 Hint 1: Consistent Percentage Hashing

Use a hash function on user ID to get consistent percentage assignment:

 1func Evaluate(ctx map[string]interface{}) bool {
 2    userID, ok := ctx["user_id"].(string)
 3    if !ok {
 4        return false
 5    }
 6
 7    hash := hashString(userID)
 8    return int(hash % 100) < pr.Percentage
 9}
10
11func hashString(s string) uint32 {
12    h := fnv.New32a()
13    h.Write([]byte(s))
14    return h.Sum32()
15}
💡 Hint 2: Thread-Safe Flag Updates

Use read-write locks for concurrent access:

 1func AddFlag(flag *Flag) {
 2    ffm.mu.Lock()
 3    defer ffm.mu.Unlock()
 4    ffm.flags[flag.Name] = flag
 5}
 6
 7func IsEnabled(name string, ctx map[string]interface{}) bool {
 8    ffm.mu.RLock()
 9    defer ffm.mu.RUnlock()
10    // Evaluation logic
11}
💡 Hint 3: Safe Type Assertions

Always use the comma-ok idiom for type assertions:

1userID, ok := ctx["user_id"].(string)
2if !ok {
3    return false // or default value
4}
💡 Hint 4: AND Rule Short-Circuit

Stop evaluating as soon as one rule fails:

1func Evaluate(ctx map[string]interface{}) bool {
2    for _, rule := range ar.Rules {
3        if !rule.Evaluate(ctx) {
4            return false // Short-circuit
5        }
6    }
7    return true
8}

Solution

Click to see the solution
  1package featureflags
  2
  3import (
  4    "hash/fnv"
  5    "strings"
  6    "sync"
  7    "time"
  8)
  9
 10// Flag represents a feature flag with evaluation rules
 11type Flag struct {
 12    Name         string
 13    Enabled      bool
 14    Description  string
 15    Rules        []Rule
 16    DefaultValue bool
 17}
 18
 19// Rule defines the interface for flag evaluation rules
 20type Rule interface {
 21    Evaluate(context map[string]interface{}) bool
 22}
 23
 24// FeatureFlagManager manages feature flags
 25type FeatureFlagManager struct {
 26    mu    sync.RWMutex
 27    flags map[string]*Flag
 28}
 29
 30// NewFeatureFlagManager creates a new feature flag manager
 31func NewFeatureFlagManager() *FeatureFlagManager {
 32    return &FeatureFlagManager{
 33        flags: make(map[string]*Flag),
 34    }
 35}
 36
 37// AddFlag adds or updates a feature flag
 38func AddFlag(flag *Flag) {
 39    ffm.mu.Lock()
 40    defer ffm.mu.Unlock()
 41    ffm.flags[flag.Name] = flag
 42}
 43
 44// RemoveFlag removes a feature flag
 45func RemoveFlag(name string) {
 46    ffm.mu.Lock()
 47    defer ffm.mu.Unlock()
 48    delete(ffm.flags, name)
 49}
 50
 51// IsEnabled checks if a feature is enabled for the given context
 52func IsEnabled(name string, context map[string]interface{}) bool {
 53    ffm.mu.RLock()
 54    defer ffm.mu.RUnlock()
 55
 56    flag, ok := ffm.flags[name]
 57    if !ok {
 58        return false
 59    }
 60
 61    // If flag is disabled, return false immediately
 62    if !flag.Enabled {
 63        return false
 64    }
 65
 66    // If no rules, return enabled state
 67    if len(flag.Rules) == 0 {
 68        return true
 69    }
 70
 71    // Evaluate all rules
 72    for _, rule := range flag.Rules {
 73        if !rule.Evaluate(context) {
 74            return false
 75        }
 76    }
 77
 78    return true
 79}
 80
 81// GetFlag retrieves a flag by name
 82func GetFlag(name string) {
 83    ffm.mu.RLock()
 84    defer ffm.mu.RUnlock()
 85    flag, ok := ffm.flags[name]
 86    return flag, ok
 87}
 88
 89// ListFlags returns all flag names
 90func ListFlags() []string {
 91    ffm.mu.RLock()
 92    defer ffm.mu.RUnlock()
 93
 94    names := make([]string, 0, len(ffm.flags))
 95    for name := range ffm.flags {
 96        names = append(names, name)
 97    }
 98    return names
 99}
100
101// PercentageRule enables feature for percentage of users
102type PercentageRule struct {
103    Percentage int // 0-100
104}
105
106func Evaluate(ctx map[string]interface{}) bool {
107    if ctx == nil {
108        return false
109    }
110
111    userID, ok := ctx["user_id"].(string)
112    if !ok {
113        return false
114    }
115
116    hash := hashString(userID)
117    return int(hash%100) < pr.Percentage
118}
119
120// hashString returns a consistent hash for a string
121func hashString(s string) uint32 {
122    h := fnv.New32a()
123    h.Write([]byte(s))
124    return h.Sum32()
125}
126
127// UserTargetingRule targets specific users
128type UserTargetingRule struct {
129    TargetUsers  []string
130    ExcludeUsers []string
131}
132
133func Evaluate(ctx map[string]interface{}) bool {
134    if ctx == nil {
135        return false
136    }
137
138    userID, ok := ctx["user_id"].(string)
139    if !ok {
140        return false
141    }
142
143    // Check exclusion list first
144    for _, excludedUser := range ut.ExcludeUsers {
145        if userID == excludedUser {
146            return false
147        }
148    }
149
150    // If no target users specified, accept all
151    if len(ut.TargetUsers) == 0 {
152        return true
153    }
154
155    // Check if user is in target list
156    for _, targetUser := range ut.TargetUsers {
157        if userID == targetUser {
158            return true
159        }
160    }
161
162    return false
163}
164
165// AttributeRule evaluates based on context attributes
166type AttributeRule struct {
167    Attribute string
168    Operator  Operator
169    Value     interface{}
170}
171
172type Operator int
173
174const (
175    Equals Operator = iota
176    NotEquals
177    GreaterThan
178    LessThan
179    Contains
180    In
181)
182
183func Evaluate(ctx map[string]interface{}) bool {
184    if ctx == nil {
185        return false
186    }
187
188    attrValue, ok := ctx[ar.Attribute]
189    if !ok {
190        return false
191    }
192
193    switch ar.Operator {
194    case Equals:
195        return attrValue == ar.Value
196
197    case NotEquals:
198        return attrValue != ar.Value
199
200    case GreaterThan:
201        if av, ok := attrValue.(int); ok {
202            if v, ok := ar.Value.(int); ok {
203                return av > v
204            }
205        }
206        return false
207
208    case LessThan:
209        if av, ok := attrValue.(int); ok {
210            if v, ok := ar.Value.(int); ok {
211                return av < v
212            }
213        }
214        return false
215
216    case Contains:
217        if av, ok := attrValue.(string); ok {
218            if v, ok := ar.Value.(string); ok {
219                return strings.Contains(av, v)
220            }
221        }
222        return false
223
224    case In:
225        if valueList, ok := ar.Value.([]string); ok {
226            if av, ok := attrValue.(string); ok {
227                for _, v := range valueList {
228                    if av == v {
229                        return true
230                    }
231                }
232            }
233        }
234        return false
235
236    default:
237        return false
238    }
239}
240
241// TimeWindowRule enables feature during specific time window
242type TimeWindowRule struct {
243    Start time.Time
244    End   time.Time
245}
246
247func Evaluate(ctx map[string]interface{}) bool {
248    now := time.Now()
249    return now.After(tw.Start) && now.Before(tw.End)
250}
251
252// AndRule requires all rules to pass
253type AndRule struct {
254    Rules []Rule
255}
256
257func Evaluate(ctx map[string]interface{}) bool {
258    for _, rule := range ar.Rules {
259        if !rule.Evaluate(ctx) {
260            return false // Short-circuit
261        }
262    }
263    return true
264}
265
266// OrRule requires any rule to pass
267type OrRule struct {
268    Rules []Rule
269}
270
271func Evaluate(ctx map[string]interface{}) bool {
272    for _, rule := range or.Rules {
273        if rule.Evaluate(ctx) {
274            return true // Short-circuit
275        }
276    }
277    return false
278}
279
280// NotRule inverts the rule result
281type NotRule struct {
282    Rule Rule
283}
284
285func Evaluate(ctx map[string]interface{}) bool {
286    return !nr.Rule.Evaluate(ctx)
287}
288
289// EmailDomainRule targets users by email domain
290type EmailDomainRule struct {
291    Domains []string // e.g., ["company.com", "partner.com"]
292}
293
294func Evaluate(ctx map[string]interface{}) bool {
295    if ctx == nil {
296        return false
297    }
298
299    email, ok := ctx["email"].(string)
300    if !ok {
301        return false
302    }
303
304    parts := strings.Split(email, "@")
305    if len(parts) != 2 {
306        return false
307    }
308
309    domain := parts[1]
310    for _, targetDomain := range ed.Domains {
311        if domain == targetDomain {
312            return true
313        }
314    }
315
316    return false
317}
318
319// EnvironmentRule enables based on environment
320type EnvironmentRule struct {
321    Environments []string // e.g., ["development", "staging"]
322}
323
324func Evaluate(ctx map[string]interface{}) bool {
325    if ctx == nil {
326        return false
327    }
328
329    env, ok := ctx["environment"].(string)
330    if !ok {
331        return false
332    }
333
334    for _, targetEnv := range er.Environments {
335        if env == targetEnv {
336            return true
337        }
338    }
339
340    return false
341}
342
343// VersionRule enables based on version comparison
344type VersionRule struct {
345    MinVersion string
346    MaxVersion string
347}
348
349func Evaluate(ctx map[string]interface{}) bool {
350    if ctx == nil {
351        return false
352    }
353
354    version, ok := ctx["version"].(string)
355    if !ok {
356        return false
357    }
358
359    // Simple string comparison
360    if vr.MinVersion != "" && version < vr.MinVersion {
361        return false
362    }
363
364    if vr.MaxVersion != "" && version > vr.MaxVersion {
365        return false
366    }
367
368    return true
369}

Key Takeaways

  • Feature flags enable safe, gradual rollouts without deploying new code
  • Percentage-based rules must use consistent hashing for predictable results
  • User targeting allows testing features with specific user groups
  • Time-based rules enable scheduled feature launches and time-limited features
  • Composite rules support complex targeting logic
  • Thread-safety is critical for production flag systems that update frequently
  • Context-based evaluation provides flexibility for different targeting strategies
  • Default values ensure graceful degradation when flags are missing