Feature flags (or feature toggles) allow you to enable/disable app features dynamically without redeploying code. This is useful for A/B testing, gradual rollouts, or quick fixes. Below, I’ll explain two approaches, with pros/cons, followed by code snippets in Spring Boot (backend) and Flutter (frontend).
Approach 1: Unified Single Endpoint (Batch Fetch)
Explanation: The backend exposes one endpoint that returns a single JSON object (e.g., a map) containing all feature flags’ states (enabled/disabled). The mobile app fetches this once (e.g., on startup) and caches it locally. This is efficient for apps with many flags, as it minimizes network calls. However, it requires updating the entire map if one flag changes, and the response can grow large.
Pros: Low latency after initial fetch; simple to implement and cache.
Cons: Over-fetches unused flags; harder to target per-user flags without segmentation.
Backend (Spring Boot) Snippet:
@RestController
@RequestMapping("/api")
public class FeatureFlagController {
@GetMapping("/flags")
public Map<String, Boolean> getAllFlags() {
Map<String, Boolean> flags = new HashMap<>();
flags.put("userProfile", true);
flags.put("darkMode", false);
flags.put("chatFeature", true);
// In production, fetch from DB/cache (e.g., Redis) based on user ID
return flags;
}
}
- This returns:
{"userProfile": true, "darkMode": false, "chatFeature": true}.
Frontend (Flutter) Snippet:
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart'; // For caching
class FeatureFlagService {
static const String _baseUrl = 'https://your-api.com/api';
static Map<String, bool> _flags = {};
static Future<void> fetchFlags() async {
try {
final response = await http.get(Uri.parse('$_baseUrl/flags'));
if (response.statusCode == 200) {
_flags = Map<String, bool>.from(json.decode(response.body));
// Cache locally
final prefs = await SharedPreferences.getInstance();
await prefs.setString('flags', json.encode(_flags));
}
} catch (e) {
// Fallback to cache
final prefs = await SharedPreferences.getInstance();
final cached = prefs.getString('flags');
if (cached != null) {
_flags = Map<String, bool>.from(json.decode(cached));
}
}
}
static bool isEnabled(String flag) => _flags[flag] ?? false;
}
// Usage in widget
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
@override
void initState() {
super.initState();
FeatureFlagService.fetchFlags(); // Call on app start
}
@override
Widget build(BuildContext context) {
if (FeatureFlagService.isEnabled('userProfile')) {
return UserProfileWidget(); // Show if enabled
}
return SizedBox.shrink(); // Hide if disabled
}
}
- Fetch once in
main.dart(e.g., viaWidgetsBinding.instance.addPostFrameCallback), then checkisEnabled()anywhere.
Approach 2: Per-Component Sub-Endpoints (Lazy/Granular Fetch)
Explanation: Each app component (e.g., a widget or feature module) has its own backend endpoint for its flag(s) and related config (e.g., the API URL for that feature). The mobile app fetches flags lazily—only when the component is about to render or on user interaction. This keeps payloads small and allows fine-grained control (e.g., user-specific flags).
Pros: Scalable for large apps; only fetch what’s needed; easy to add endpoints without touching others.
Cons: More network calls (potential latency); requires careful error handling per fetch.
Backend (Spring Boot) Snippet:
@RestController
@RequestMapping("/api")
public class FeatureFlagController {
// Example for "userProfile" feature
@GetMapping("/flags/user-profile")
public FeatureConfig getUserProfileFlag() {
// In production, check DB/cache for user-specific flag
return new FeatureConfig(true, "https://your-api.com/user/profile"); // Enabled + config
}
// Example for "chat" feature
@GetMapping("/flags/chat")
public FeatureConfig getChatFlag() {
return new FeatureConfig(false, "https://your-api.com/chat/ws"); // Disabled + config
}
}
// Simple DTO for flag + config
@Data
@AllArgsConstructor
class FeatureConfig {
private boolean enabled;
private String endpoint;
}
- This returns (for
/flags/user-profile):{"enabled": true, "endpoint": "https://your-api.com/user/profile"}.
Frontend (Flutter) Snippet:
import 'package:http/http.dart' as http;
import 'dart:convert';
class FeatureConfig {
final bool enabled;
final String endpoint;
FeatureConfig({required this.enabled, required this.endpoint});
}
class FeatureFlagService {
static const String _baseUrl = 'https://your-api.com/api';
static Future<FeatureConfig?> fetchFlag(String feature) async {
try {
final response = await http.get(Uri.parse('$_baseUrl/flags/$feature'));
if (response.statusCode == 200) {
final data = json.decode(response.body);
return FeatureConfig(
enabled: data['enabled'],
endpoint: data['endpoint'],
);
}
} catch (e) {
// Handle error, e.g., default to disabled
}
return null; // Or default: FeatureConfig(enabled: false, endpoint: '')
}
}
// Usage in a modular widget
class UserProfileWidget extends StatefulWidget {
@override
_UserProfileWidgetState createState() => _UserProfileWidgetState();
}
class _UserProfileWidgetState extends State<UserProfileWidget> {
FeatureConfig? _config;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadFlag();
}
Future<void> _loadFlag() async {
_config = await FeatureFlagService.fetchFlag('user-profile');
setState(() => _isLoading = false);
}
@override
Widget build(BuildContext context) {
if (_isLoading) return CircularProgressIndicator();
if (_config?.enabled != true) return SizedBox.shrink(); // Hide if disabled
return FutureBuilder<String>(
future: http.get(Uri.parse(_config!.endpoint)).then((r) => r.body), // Use the config endpoint
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text('Profile Data: ${snapshot.data}'); // Render feature
}
return SizedBox.shrink();
},
);
}
}
- Each widget fetches its own flag on init; use a provider (e.g., Riverpod) for caching across instances to avoid redundant calls.
These snippets are minimal—add authentication (e.g., JWT), error handling, and caching (e.g., Hive for Flutter) in production.
