devops-automation feature flagsdatabase configurationdeployment strategies

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.

📖 21 min read 📅 February 28, 2026 ✍ By PropTechUSA AI
21m
Read Time
4.2k
Words
17
Sections

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:

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

async isEnabled(flagKey: string, context: EvaluationContext): Promise<boolean> {

const config = await this.getFlagConfiguration(flagKey);

return this.evaluateFlag(config, context);

}

private async getFlagConfiguration(flagKey: string): Promise<FlagConfiguration> {

const cached = this.cache.get(flagKey);

if (cached && !this.isCacheExpired(cached)) {

return cached;

}

const config = await this.loadFromDatabase(flagKey);

this.cache.set(flagKey, {

...config,

cachedAt: Date.now()

});

return config;

}

private evaluateFlag(config: FlagConfiguration, context: EvaluationContext): boolean {

// Check user-specific segments first

for (const segment of config.segments) {

if (this.matchesSegment(segment, context)) {

return segment.enabled;

}

}

// Fall back to rollout percentage

if (config.rolloutPercentage > 0) {

const hash = this.hashContext(config.key, context.userId);

return (hash % 100) < config.rolloutPercentage;

}

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('flag_updates');

this.redis.on('message', this.handleCacheInvalidation.bind(this));

}

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

if (channel === 'flag_updates') {

const { flagKey } = JSON.parse(message);

this.flagService.invalidateCache(flagKey);

}

}

async updateFlag(flagKey: string, config: FlagConfiguration): Promise<void> {

await this.database.updateFlag(flagKey, config);

// Notify all instances to invalidate cache

await this.redis.publish('flag_updates', 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 {

const crypto = require('crypto');

const hash = crypto.createHash('md5')

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

.digest('hex');

return parseInt(hash.substring(0, 8), 16);

}

evaluateRollout(

flagKey: string,

userId: string,

rolloutPercentage: number

): boolean {

if (rolloutPercentage >= 100) return true;

if (rolloutPercentage <= 0) return false;

const userHash = this.hashUser(flagKey, userId);

const bucket = userHash % 100;

return bucket < rolloutPercentage;

}

async evaluateWithRampUp(

flagKey: string,

userId: string,

schedule: RolloutSchedule

): Promise<boolean> {

const now = new Date();

const currentPercentage = this.calculateCurrentPercentage(schedule, now);

return this.evaluateRollout(flagKey, userId, currentPercentage);

}

private calculateCurrentPercentage(schedule: RolloutSchedule, now: Date): number {

if (now < schedule.startDate) return 0;

if (now > schedule.endDate) return schedule.targetPercentage;

const totalDuration = schedule.endDate.getTime() - schedule.startDate.getTime();

const elapsed = now.getTime() - schedule.startDate.getTime();

const progress = elapsed / totalDuration;

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 {

async evaluateForTenant(

flagKey: string,

context: TenantContext

): Promise<boolean> {

const flagConfig = await this.getFlagConfiguration(flagKey);

// Check tenant-specific overrides first

const tenantOverride = await this.getTenantOverride(flagKey, context.tenantId);

if (tenantOverride !== null) {

return tenantOverride.enabled;

}

// Evaluate subscription tier restrictions

if (flagConfig.requiredTier &&

!this.hasRequiredTier(context.subscriptionTier, flagConfig.requiredTier)) {

return false;

}

// Check regional availability

if (flagConfig.enabledRegions.length > 0 &&

!flagConfig.enabledRegions.includes(context.region)) {

return false;

}

// Standard user-based evaluation

return this.evaluateStandard(flagConfig, context.userId);

}

private async getTenantOverride(

flagKey: string,

tenantId: string

): Promise<TenantOverride | null> {

const query =

SELECT enabled FROM tenant_flag_overrides

WHERE flag_key = $1 AND tenant_id = $2

;

const result = await this.db.query(query, [flagKey, tenantId]);

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 {

async getVariant(

testKey: string,

userId: string

): Promise<ABTestVariant> {

const testConfig = await this.getTestConfiguration(testKey);

if (!testConfig.enabled) {

return testConfig.controlVariant;

}

const userHash = this.hashUser(testKey, userId);

const bucket = userHash % 1000; // Use 1000 for finer granularity

let cumulativeWeight = 0;

for (const variant of testConfig.variants) {

cumulativeWeight += variant.weight * 10; // Convert percentage to per-mille

if (bucket < cumulativeWeight) {

// Log assignment for analytics

await this.logVariantAssignment(testKey, userId, variant.key);

return variant;

}

}

return testConfig.controlVariant;

}

async evaluateFeatureForVariant(

featureKey: string,

testKey: string,

userId: string

): Promise<any> {

const variant = await this.getVariant(testKey, userId);

const featureConfig = variant.configuration[featureKey];

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 {

async auditStaleFl flags(): Promise<StaleFlag[]> {

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 '90 days'

AND f.temporary = true

ORDER BY f.updated_at ASC

;

const results = await this.db.query(query);

return results.rows.map(row => ({

flagKey: row.key,

daysSinceUpdate: row.days_since_update,

environments: row.environment,

enabled: row.enabled

}));

}

async scheduleCleanup(flagKey: string, removalDate: Date): Promise<void> {

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 TipSet 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();

async recordEvaluation(

flagKey: string,

evaluationTimeMs: number,

cacheHit: boolean

): Promise<void> {

const metrics = this.metrics.get(flagKey) || {

evaluationCount: 0,

totalTimeMs: 0,

cacheHitRate: 0,

cacheHits: 0

};

metrics.evaluationCount++;

metrics.totalTimeMs += evaluationTimeMs;

if (cacheHit) {

metrics.cacheHits++;

}

metrics.cacheHitRate = (metrics.cacheHits / metrics.evaluationCount) * 100;

this.metrics.set(flagKey, metrics);

// Alert on performance degradation

if (evaluationTimeMs > 100) { // > 100ms is concerning

await this.alertSlowEvaluation(flagKey, evaluationTimeMs);

}

}

generatePerformanceReport(): FlagPerformanceReport {

const flags = Array.from(this.metrics.entries()).map(([key, metrics]) => ({

flagKey: key,

avgEvaluationTimeMs: metrics.totalTimeMs / metrics.evaluationCount,

cacheHitRate: metrics.cacheHitRate,

evaluationCount: metrics.evaluationCount

}));

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:

⚠️
WarningNever 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 {

async preDeploymentCheck(deploymentFlags: string[]): Promise<boolean> {

for (const flagKey of deploymentFlags) {

const flag = await this.flagService.getFlag(flagKey);

if (!flag) {

throw new Error(Required flag ${flagKey} not found);

}

// Ensure flag exists in target environment

const envConfig = await this.flagService.getEnvironmentConfig(

flagKey,

process.env.DEPLOY_ENVIRONMENT

);

if (!envConfig) {

await this.flagService.createEnvironmentConfig(

flagKey,

process.env.DEPLOY_ENVIRONMENT,

{ enabled: false, rolloutPercentage: 0 }

);

}

}

return true;

}

async enableFlagsForDeployment(flags: Array<{key: string, percentage: number}>): Promise<void> {

for (const {key, percentage} of flags) {

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.

🚀 Ready to Build?

Let's discuss how we can help with your project.

Start Your Project →