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:
- Inconsistent hashing: Same user should always get same percentage result
- Race conditions: Flag updates must be thread-safe
- Missing context: Always handle nil/missing context values gracefully
- Type assertions: Context values can be any type - validate before using
- Rule evaluation order: AND rules should short-circuit on first false
- 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