DevOps & Automation

Feature Flags with Database-Driven Configuration Guide

Master database-driven feature flags for dynamic deployment strategies. Learn implementation patterns, configuration management, and best practices for PropTech.

· By PropTechUSA AI
21m
Read Time
4.2k
Words
5
Sections
9
Code Examples

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.

sql
-- 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, -- 'user_id', 'property_type', 'region'

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:

typescript
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:

typescript
class DistributedFlagCache {

constructor(

private redis: RedisClient,

private flagService: FeatureFlagService

) {

this.redis.subscribe(&#039;flag_updates&#039;);

this.redis.on(&#039;message&#039;, this.handleCacheInvalidation.bind(this));

}

private handleCacheInvalidation(channel: string, message: string): void {

class="kw">if (channel === &#039;flag_updates&#039;) {

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_updates&#039;, 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:

typescript
class GradualRolloutEvaluator {

private hashUser(flagKey: string, userId: string): number {

class="kw">const crypto = require(&#039;crypto&#039;);

class="kw">const hash = crypto.createHash(&#039;md5&#039;)

.update(${flagKey}:${userId})

.digest(&#039;hex&#039;);

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:

typescript
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:

typescript
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:

typescript
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 days&#039;

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]);

}

}

💡
Pro Tip
Set explicit expiration dates for temporary flags and implement automated alerts for flags that haven't been updated recently. This prevents abandoned flags from cluttering your codebase.

Performance Monitoring and Optimization

Feature flag evaluation can become a performance bottleneck if not properly optimized. Implement comprehensive monitoring:

typescript
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
⚠️
Warning
Never expose flag evaluation logic or configuration details to client-side applications. Always evaluate flags server-side and return only the necessary boolean or variant results.

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:

typescript
// 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.

Need This Built?
We build production-grade systems with the exact tech covered in this article.
Start Your Project
PT
PropTechUSA.ai Engineering
Technical Content
Deep technical content from the team building production systems with Cloudflare Workers, AI APIs, and modern web infrastructure.