Modern software development demands the ability to deploy code without immediately exposing new features to users. Feature flags have revolutionized how development teams approach deployment strategies, enabling safer releases, A/B testing, and gradual rollouts. While file-based configuration works for simple scenarios, database-driven feature flags offer the dynamic control and real-time management capabilities that enterprise applications require.
Understanding Database-Driven Feature Flags
Feature flags, also known as feature toggles or feature switches, are conditional statements that control whether specific functionality is enabled or disabled at runtime. Unlike traditional deployment approaches where features go live immediately upon code deployment, feature flags decouple feature releases from code deployments.
The Evolution from Static to Dynamic Configuration
Traditional feature flag implementations often rely on configuration files, environment variables, or hardcoded values. While these approaches work for basic use cases, they present significant limitations in complex environments:
- Deployment dependency: Changing flag states requires new deployments
- Limited targeting: Difficult to enable features for specific user segments
- Poor auditability: No clear history of configuration changes
- Synchronization challenges: Multiple services may have inconsistent states
Database-driven configuration addresses these limitations by centralizing flag management in a persistent data store, enabling real-time updates without service restarts.
Core Components of Database-Driven Systems
A robust database-driven feature flag system consists of several key components working together:
Configuration Storage Layer: The database schema that stores flag definitions, rules, and targeting criteria. This typically includes tables for flags, user segments, rollout percentages, and audit logs. Evaluation Engine: The runtime component that queries configuration data and determines whether a flag should be enabled for a specific context (user, request, environment). Management Interface: Administrative tools for non-technical stakeholders to manage flag states, create user segments, and monitor flag usage. Caching Layer: Performance optimization that reduces database load while maintaining reasonable consistency guarantees.Core Architecture Patterns
Implementing feature flags with database configuration requires careful consideration of architecture patterns that balance performance, consistency, and operational complexity.
Database Schema Design
The foundation of any database-driven feature flag system is a well-designed schema that supports both simple boolean flags and complex targeting rules.
-- Core feature flags table
CREATE TABLE feature_flags(
id SERIAL PRIMARY KEY,
key VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
description TEXT,
default_enabled BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by INTEGER REFERENCES users(id)
);
-- Environment-specific overrides
CREATE TABLE flag_environments(
id SERIAL PRIMARY KEY,
flag_id INTEGER REFERENCES feature_flags(id),
environment VARCHAR(50) NOT NULL,
enabled BOOLEAN NOT NULL,
rollout_percentage INTEGER DEFAULT 0,
UNIQUE(flag_id, environment)
);
-- User segment targeting
CREATE TABLE flag_segments(
id SERIAL PRIMARY KEY,
flag_id INTEGER REFERENCES feature_flags(id),
environment VARCHAR(50) NOT NULL,
segment_type VARCHAR(50) NOT NULL, -- 039;user_id039;, 039;property_type039;, 039;region039;
segment_value VARCHAR(255) NOT NULL,
enabled BOOLEAN NOT NULL
);
This schema supports both simple environment-based toggles and sophisticated user segmentation, crucial for PropTech applications where features might roll out differently based on property types, geographic regions, or user roles.
Caching Strategies
Direct database queries for every feature flag evaluation would create unacceptable performance overhead. Effective caching strategies are essential:
class FeatureFlagService {
private cache: Map<string, FlagConfiguration> = new Map();
private cacheTimeout = 30000; // 30 seconds
class="kw">async isEnabled(flagKey: string, context: EvaluationContext): Promise<boolean> {
class="kw">const config = class="kw">await this.getFlagConfiguration(flagKey);
class="kw">return this.evaluateFlag(config, context);
}
private class="kw">async getFlagConfiguration(flagKey: string): Promise<FlagConfiguration> {
class="kw">const cached = this.cache.get(flagKey);
class="kw">if (cached && !this.isCacheExpired(cached)) {
class="kw">return cached;
}
class="kw">const config = class="kw">await this.loadFromDatabase(flagKey);
this.cache.set(flagKey, {
...config,
cachedAt: Date.now()
});
class="kw">return config;
}
private evaluateFlag(config: FlagConfiguration, context: EvaluationContext): boolean {
// Check user-specific segments first
class="kw">for (class="kw">const segment of config.segments) {
class="kw">if (this.matchesSegment(segment, context)) {
class="kw">return segment.enabled;
}
}
// Fall back to rollout percentage
class="kw">if (config.rolloutPercentage > 0) {
class="kw">const hash = this.hashContext(config.key, context.userId);
class="kw">return (hash % 100) < config.rolloutPercentage;
}
class="kw">return config.defaultEnabled;
}
}
Distributed Cache Invalidation
In multi-instance deployments, cache invalidation becomes critical for consistency. Database triggers or message queues can notify application instances when flag configurations change:
class DistributedFlagCache {
constructor(
private redis: RedisClient,
private flagService: FeatureFlagService
) {
this.redis.subscribe(039;flag_updates039;);
this.redis.on(039;message039;, this.handleCacheInvalidation.bind(this));
}
private handleCacheInvalidation(channel: string, message: string): void {
class="kw">if (channel === 039;flag_updates039;) {
class="kw">const { flagKey } = JSON.parse(message);
this.flagService.invalidateCache(flagKey);
}
}
class="kw">async updateFlag(flagKey: string, config: FlagConfiguration): Promise<void> {
class="kw">await this.database.updateFlag(flagKey, config);
// Notify all instances to invalidate cache
class="kw">await this.redis.publish(039;flag_updates039;, JSON.stringify({ flagKey }));
}
}
Implementation Examples and Patterns
Real-world feature flag implementations must handle diverse scenarios, from simple on/off switches to complex business logic. Here are proven patterns for common PropTech use cases.
Gradual Rollout Implementation
Gradual rollouts are essential for validating new features with minimal risk. This example shows how to implement percentage-based rollouts with user stickiness:
class GradualRolloutEvaluator {
private hashUser(flagKey: string, userId: string): number {
class="kw">const crypto = require(039;crypto039;);
class="kw">const hash = crypto.createHash(039;md5039;)
.update(${flagKey}:${userId})
.digest(039;hex039;);
class="kw">return parseInt(hash.substring(0, 8), 16);
}
evaluateRollout(
flagKey: string,
userId: string,
rolloutPercentage: number
): boolean {
class="kw">if (rolloutPercentage >= 100) class="kw">return true;
class="kw">if (rolloutPercentage <= 0) class="kw">return false;
class="kw">const userHash = this.hashUser(flagKey, userId);
class="kw">const bucket = userHash % 100;
class="kw">return bucket < rolloutPercentage;
}
class="kw">async evaluateWithRampUp(
flagKey: string,
userId: string,
schedule: RolloutSchedule
): Promise<boolean> {
class="kw">const now = new Date();
class="kw">const currentPercentage = this.calculateCurrentPercentage(schedule, now);
class="kw">return this.evaluateRollout(flagKey, userId, currentPercentage);
}
private calculateCurrentPercentage(schedule: RolloutSchedule, now: Date): number {
class="kw">if (now < schedule.startDate) class="kw">return 0;
class="kw">if (now > schedule.endDate) class="kw">return schedule.targetPercentage;
class="kw">const totalDuration = schedule.endDate.getTime() - schedule.startDate.getTime();
class="kw">const elapsed = now.getTime() - schedule.startDate.getTime();
class="kw">const progress = elapsed / totalDuration;
class="kw">return Math.floor(schedule.targetPercentage * progress);
}
}
Multi-Tenant Feature Control
PropTech applications often serve multiple client organizations with different feature requirements. Database-driven flags excel at tenant-specific configuration:
interface TenantContext {
tenantId: string;
userId: string;
propertyTypes: string[];
region: string;
subscriptionTier: string;
}
class TenantAwareFeatureFlags {
class="kw">async evaluateForTenant(
flagKey: string,
context: TenantContext
): Promise<boolean> {
class="kw">const flagConfig = class="kw">await this.getFlagConfiguration(flagKey);
// Check tenant-specific overrides first
class="kw">const tenantOverride = class="kw">await this.getTenantOverride(flagKey, context.tenantId);
class="kw">if (tenantOverride !== null) {
class="kw">return tenantOverride.enabled;
}
// Evaluate subscription tier restrictions
class="kw">if (flagConfig.requiredTier &&
!this.hasRequiredTier(context.subscriptionTier, flagConfig.requiredTier)) {
class="kw">return false;
}
// Check regional availability
class="kw">if (flagConfig.enabledRegions.length > 0 &&
!flagConfig.enabledRegions.includes(context.region)) {
class="kw">return false;
}
// Standard user-based evaluation
class="kw">return this.evaluateStandard(flagConfig, context.userId);
}
private class="kw">async getTenantOverride(
flagKey: string,
tenantId: string
): Promise<TenantOverride | null> {
class="kw">const query =
SELECT enabled FROM tenant_flag_overrides
WHERE flag_key = $1 AND tenant_id = $2
;
class="kw">const result = class="kw">await this.db.query(query, [flagKey, tenantId]);
class="kw">return result.rows[0] || null;
}
}
A/B Testing Integration
Feature flags naturally extend to support A/B testing scenarios. This implementation shows how to manage multiple variants:
interface ABTestVariant {
key: string;
name: string;
weight: number;
configuration: Record<string, any>;
}
class ABTestingEvaluator {
class="kw">async getVariant(
testKey: string,
userId: string
): Promise<ABTestVariant> {
class="kw">const testConfig = class="kw">await this.getTestConfiguration(testKey);
class="kw">if (!testConfig.enabled) {
class="kw">return testConfig.controlVariant;
}
class="kw">const userHash = this.hashUser(testKey, userId);
class="kw">const bucket = userHash % 1000; // Use 1000 class="kw">for finer granularity
class="kw">let cumulativeWeight = 0;
class="kw">for (class="kw">const variant of testConfig.variants) {
cumulativeWeight += variant.weight * 10; // Convert percentage to per-mille
class="kw">if (bucket < cumulativeWeight) {
// Log assignment class="kw">for analytics
class="kw">await this.logVariantAssignment(testKey, userId, variant.key);
class="kw">return variant;
}
}
class="kw">return testConfig.controlVariant;
}
class="kw">async evaluateFeatureForVariant(
featureKey: string,
testKey: string,
userId: string
): Promise<any> {
class="kw">const variant = class="kw">await this.getVariant(testKey, userId);
class="kw">const featureConfig = variant.configuration[featureKey];
class="kw">return featureConfig !== undefined ? featureConfig : false;
}
}
Best Practices and Operational Considerations
Successful database-driven feature flag implementations require attention to operational concerns, performance optimization, and team workflows.
Flag Lifecycle Management
Feature flags are not permanent fixtures in your codebase. Establishing clear lifecycle management practices prevents technical debt accumulation:
class FlagLifecycleManager {
class="kw">async auditStaleFl flags(): Promise<StaleFlag[]> {
class="kw">const query =
SELECT f.*,
fe.environment,
fe.enabled,
EXTRACT(DAYS FROM NOW() - f.updated_at) as days_since_update
FROM feature_flags f
JOIN flag_environments fe ON f.id = fe.flag_id
WHERE f.updated_at < NOW() - INTERVAL 039;90 days039;
AND f.temporary = true
ORDER BY f.updated_at ASC
;
class="kw">const results = class="kw">await this.db.query(query);
class="kw">return results.rows.map(row => ({
flagKey: row.key,
daysSinceUpdate: row.days_since_update,
environments: row.environment,
enabled: row.enabled
}));
}
class="kw">async scheduleCleanup(flagKey: string, removalDate: Date): Promise<void> {
class="kw">await this.db.query(
INSERT INTO flag_cleanup_schedule(flag_key, scheduled_removal)
VALUES($1, $2)
ON CONFLICT(flag_key) DO UPDATE
SET scheduled_removal = $2
, [flagKey, removalDate]);
}
}
Performance Monitoring and Optimization
Feature flag evaluation can become a performance bottleneck if not properly optimized. Implement comprehensive monitoring:
class FlagPerformanceMonitor {
private metrics: Map<string, FlagMetrics> = new Map();
class="kw">async recordEvaluation(
flagKey: string,
evaluationTimeMs: number,
cacheHit: boolean
): Promise<void> {
class="kw">const metrics = this.metrics.get(flagKey) || {
evaluationCount: 0,
totalTimeMs: 0,
cacheHitRate: 0,
cacheHits: 0
};
metrics.evaluationCount++;
metrics.totalTimeMs += evaluationTimeMs;
class="kw">if (cacheHit) {
metrics.cacheHits++;
}
metrics.cacheHitRate = (metrics.cacheHits / metrics.evaluationCount) * 100;
this.metrics.set(flagKey, metrics);
// Alert on performance degradation
class="kw">if (evaluationTimeMs > 100) { // > 100ms is concerning
class="kw">await this.alertSlowEvaluation(flagKey, evaluationTimeMs);
}
}
generatePerformanceReport(): FlagPerformanceReport {
class="kw">const flags = Array.from(this.metrics.entries()).map(([key, metrics]) => ({
flagKey: key,
avgEvaluationTimeMs: metrics.totalTimeMs / metrics.evaluationCount,
cacheHitRate: metrics.cacheHitRate,
evaluationCount: metrics.evaluationCount
}));
class="kw">return {
flags,
totalEvaluations: flags.reduce((sum, f) => sum + f.evaluationCount, 0),
avgCacheHitRate: flags.reduce((sum, f) => sum + f.cacheHitRate, 0) / flags.length
};
}
}
Security and Access Control
Database-driven feature flags require robust security measures, especially in multi-tenant environments:
- Audit logging: Track all flag configuration changes with user attribution
- Role-based permissions: Limit who can modify production flags
- Change approval workflows: Require peer review for critical flag modifications
- Environment isolation: Prevent accidental production changes during development
Integration with CI/CD Pipelines
Feature flags should integrate seamlessly with your deployment pipeline. At PropTechUSA.ai, we've found that automated flag management reduces deployment risks significantly:
// Example CI/CD integration script
class DeploymentFlagManager {
class="kw">async preDeploymentCheck(deploymentFlags: string[]): Promise<boolean> {
class="kw">for (class="kw">const flagKey of deploymentFlags) {
class="kw">const flag = class="kw">await this.flagService.getFlag(flagKey);
class="kw">if (!flag) {
throw new Error(Required flag ${flagKey} not found);
}
// Ensure flag exists in target environment
class="kw">const envConfig = class="kw">await this.flagService.getEnvironmentConfig(
flagKey,
process.env.DEPLOY_ENVIRONMENT
);
class="kw">if (!envConfig) {
class="kw">await this.flagService.createEnvironmentConfig(
flagKey,
process.env.DEPLOY_ENVIRONMENT,
{ enabled: false, rolloutPercentage: 0 }
);
}
}
class="kw">return true;
}
class="kw">async enableFlagsForDeployment(flags: Array<{key: string, percentage: number}>): Promise<void> {
class="kw">for (class="kw">const {key, percentage} of flags) {
class="kw">await this.flagService.updateRolloutPercentage(
key,
process.env.DEPLOY_ENVIRONMENT,
percentage
);
console.log(Enabled flag ${key} at ${percentage}% rollout);
}
}
}
Conclusion and Next Steps
Database-driven feature flags represent a mature approach to deployment strategies that enables safer releases, better user experiences, and more agile development practices. By centralizing configuration in a database, teams gain the flexibility to respond quickly to changing requirements while maintaining strict control over feature exposure.
The patterns and implementations discussed here provide a foundation for building robust feature flag systems. However, the specific needs of your PropTech application may require additional considerations around data privacy, compliance requirements, or integration with existing property management systems.
Successful feature flag adoption requires both technical implementation and organizational change. Start with simple boolean flags for low-risk features, gradually introducing more sophisticated targeting and A/B testing capabilities as your team gains confidence with the approach.
At PropTechUSA.ai, we've seen firsthand how effective feature flag strategies accelerate development cycles while reducing deployment risks. The investment in database-driven configuration pays dividends in operational flexibility and development velocity.
Ready to implement feature flags in your PropTech application? Begin with a pilot project using the database schema and caching patterns outlined above, then expand to more complex scenarios as your team develops expertise with the tools and workflows.