Skip to main content

Configuration Framework Design Patterns and Implementation

Comprehensive low-level design for a scalable configuration management system with dynamic updates, validation, and multi-source support.

A robust configuration framework is essential for modern distributed systems, enabling centralized management, real-time updates, and consistent validation across services while maintaining high availability and fault tolerance.

1. Problem Statement & Requirements

Clarifying questions

  1. What scale and usage patterns are expected? Will this configuration framework support thousands of services with potentially millions of configuration values, or is it for a smaller-scale internal tooling?
  2. What consistency requirements are there? Should configuration changes be immediately available across all services (strong consistency) or can eventual consistency be acceptable with a small delay?
  3. What are the key entities in the configuration system? Are we dealing with simple key-value pairs, hierarchical configurations (e.g., environment-specific), or complex nested structures?
  4. What is the read/write ratio? Is this primarily a read-heavy system (e.g., services constantly polling for config) or more balanced?
  5. What latency sensitivity is acceptable? Should configuration updates be reflected in milliseconds or can they tolerate a few seconds?
  6. What authentication/authorization model is expected? Will this be used internally with service-to-service auth, or will it support user access with different permission levels?
  7. Are there any external integrations or dependencies? Will this integrate with service discovery tools, orchestration platforms (like Kubernetes), or external configuration management systems?

Functional requirements

  • Configuration storage and retrieval: Store and retrieve key-value pairs or hierarchical configuration data
  • Configuration versioning: Support for multiple versions of configurations with ability to roll back to previous versions
  • Environment-specific configuration: Allow configuration values to vary by environment (dev, staging, prod)
  • Configuration validation: Validate configuration values against defined schemas or constraints
  • Real-time configuration updates: Support for push-based configuration updates to services (e.g., via WebSockets or long polling)
  • Configuration change history: Track who made changes and when
  • Configuration inheritance: Support for configuration inheritance from parent environments or groups

Non-functional requirements

  • Latency targets:
    • p50: <50ms for read operations
    • p95: <200ms for read operations
    • p99: <500ms for read operations
  • Throughput:
    • 10,000+ concurrent configuration reads per second
    • 1,000+ configuration updates per second
  • Availability:
    • 99.9% uptime for read and write operations
    • Support for graceful degradation during partial outages

Constraints and assumptions

  • Assumption: Configuration data is not highly sensitive (e.g., no secrets or PII)
  • Constraint: Configuration framework will be used across multiple services and environments
  • Assumption: Services using the configuration will support periodic polling or long-polling for updates
  • Constraint: Configuration changes should be atomic and consistent across all consumers
  • Assumption: The system will run in a cloud-native environment (e.g., Kubernetes, microservices architecture)
  • Constraint: Configuration updates should not cause service downtime or restarts

Out of scope

  • Secret management: Storing sensitive data like passwords, API keys, or encryption keys (this should be handled by a dedicated secrets manager)
  • Configuration schema definition: Not responsible for defining or validating configuration schemas (should be handled by service-specific validators)
  • Configuration encryption: Not responsible for encrypting configuration data at rest or in transit (should be handled by infrastructure or service layer)
  • User interface: No UI for managing configurations; only API and CLI support
  • Multi-region replication: Cross-region configuration distribution is out of scope
  • Configuration drift detection: Not responsible for detecting when services have diverged from the configuration state

2. API Design

API Style Selection

For a configuration framework, I'll select REST API style with JSON payloads for the following reasons:

  • Broad industry adoption and developer familiarity
  • Clear resource-based semantics (configuration, environment, etc.)
  • Good tooling support for documentation and testing
  • Compatible with existing infrastructure (API gateways, monitoring tools)
  • Simpler to implement and debug compared to GraphQL or gRPC

Endpoint Definitions

MethodPathDescriptionAuth
GET/v1/configsList all configuration itemsJWT
GET/v1/configs/{id}Get specific config by IDJWT
POST/v1/configsCreate new configuration itemJWT + RBAC
PUT/v1/configs/{id}Update existing config by IDJWT + RBAC
DELETE/v1/configs/{id}Delete config by IDJWT + RBAC
GET/v1/configs/{id}/historyGet version history of configJWT
POST/v1/configs/{id}/revertRevert to a previous versionJWT + RBAC
GET/v1/environmentsList all environmentsJWT
GET/v1/environments/{id}Get environment detailsJWT
POST/v1/environmentsCreate new environmentJWT + RBAC
GET/v1/validatorsList all validator rulesJWT
POST/v1/validatorsCreate new validator ruleJWT + RBAC

Request and Response Schemas

Create Configuration Request:

{
  "name": "database.connection.timeout",
  "value": "30",
  "type": "integer",
  "environmentId": "env-123",
  "description": "Database connection timeout in seconds",
  "tags": ["database", "timeout"],
  "validators": [
    {
      "rule": "range",
      "min": 1,
      "max": 300
    }
  ]
}

Create Configuration Response:

{
  "id": "cfg-456",
  "name": "database.connection.timeout",
  "value": "30",
  "type": "integer",
  "environmentId": "env-123",
  "description": "Database connection timeout in seconds",
  "tags": ["database", "timeout"],
  "version": 1,
  "createdAt": "2023-10-15T14:30:00Z",
  "updatedAt": "2023-10-15T14:30:00Z"
}

Environment Response:

{
  "id": "env-123",
  "name": "production",
  "description": "Production environment",
  "createdAt": "2023-10-15T14:30:00Z",
  "updatedAt": "2023-10-15T14:30:00Z"
}

HTTP Status Codes and Error Format

Standard Status Codes:

  • 200 OK - Successful GET, PUT, PATCH
  • 201 Created - Successful POST
  • 204 No Content - Successful DELETE
  • 400 Bad Request - Invalid input or validation error
  • 401 Unauthorized - Missing or invalid auth token
  • 403 Forbidden - Insufficient permissions
  • 404 Not Found - Resource not found
  • 422 Unprocessable Entity - Validation error
  • 500 Internal Server Error - Unexpected server error

Standardized Error Response:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid configuration value: must be between 1 and 300",
    "details": [
      {
        "field": "value",
        "message": "must be between 1 and 300"
      }
    ],
    "timestamp": "2023-10-15T14:30:00Z",
    "requestId": "req-789"
  }
}

API Versioning Strategy

Versioning will be done via path prefix: /v1/ with future versions as /v2/, etc. This approach:

  • Is simple to implement and understand
  • Allows for clean separation of concerns between versions
  • Supports gradual migration of clients to new versions

Pagination Design

Using cursor-based pagination with the following design:

  • after parameter for forward pagination
  • limit parameter (default 20, max 100)
  • orderBy parameter (created_at, name, etc.)
  • Cursor format: base64 encoded JSON with id and createdAt

Example:

GET /v1/configs?after=eyJpZCI6ImNnZi0xMjMiLCJjcmVhdGVkQXQiOiIyMDIzLTEwLTE1VDE0OjMwOjAwWiJ9&limit=20

Authentication and Authorization

Authentication:

  • JWT tokens with 1-hour expiration (configurable)
  • OAuth2.0 integration for enterprise customers
  • API key support for service-to-service communication

Authorization:

  • RBAC model with roles: ADMIN, EDITOR, VIEWER
  • Permissions tied to environments and configuration groups
  • Fine-grained access control per configuration item

Rate Limiting Strategy

Rate Limits:

  • 1000 requests per hour per client (API key or user)
  • 50 requests per minute for anonymous access
  • 1000 concurrent connections limit

Enforcement:

  • Token bucket algorithm for rate limiting
  • Distributed implementation using Redis
  • Client-side retry logic with exponential backoff

Idempotency Design

Idempotency Key Strategy:

  • X-Idempotency-Key header for all mutating operations
  • Key format: {user-id}-{client-provided-key}
  • Store in Redis with TTL of 24 hours
  • For duplicate requests, return the same response as first request
// Example implementation in Java
public class IdempotencyService {
    private final RedisTemplate<String, String> redisTemplate;
    
    public ResponseEntity<?> handleIdempotentRequest(
            String idempotencyKey, 
            Supplier<ResponseEntity<?>> operation) {
        
        String cacheKey = "idempotency:" + idempotencyKey;
        String cachedResponse = redisTemplate.opsForValue().get(cacheKey);
        
        if (cachedResponse != null) {
            return ResponseEntity.status(HttpStatus.OK).body(cachedResponse);
        }
        
        ResponseEntity<?> response = operation.get();
        redisTemplate.opsForValue().set(
            cacheKey, 
            response.getBody().toString(), 
            Duration.ofHours(24)
        );
        
        return response;
    }
}

3. Data Model Design

Entity-Relationship Diagram

erDiagram
    CONFIGURATION {
        string id PK
        string name
        string description
        string value
        string type
        string environment
        string owner
        boolean is_active
        timestamp created_at
        timestamp updated_at
        timestamp deleted_at
    }

    CONFIGURATION_GROUP {
        string id PK
        string name
        string description
        timestamp created_at
        timestamp updated_at
    }

    CONFIGURATION_GROUP_MEMBER {
        string config_id FK
        string group_id FK
        timestamp created_at
    }

    CONFIGURATION_HISTORY {
        string id PK
        string config_id FK
        string old_value
        string new_value
        string changed_by
        timestamp changed_at
    }

    CONFIGURATION {
        string id PK
        string name
        string description
        string value
        string type
        string environment
        string owner
        boolean is_active
        timestamp created_at
        timestamp updated_at
        timestamp deleted_at
    }

    CONFIGURATION_GROUP_MEMBER ||--o{ CONFIGURATION : "belongs to"
    CONFIGURATION_GROUP ||--o{ CONFIGURATION_GROUP_MEMBER : "contains"
    CONFIGURATION ||--o{ CONFIGURATION_HISTORY : "has history"

Table Schema Definitions

-- Main configuration table
CREATE TABLE configurations (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    description TEXT,
    value TEXT NOT NULL,
    type VARCHAR(50) NOT NULL, -- 'string', 'number', 'boolean', 'json'
    environment VARCHAR(100) NOT NULL,
    owner VARCHAR(255),
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    deleted_at TIMESTAMP WITH TIME ZONE,
    
    -- Indexes
    INDEX idx_config_name (name),
    INDEX idx_config_environment (environment),
    INDEX idx_config_owner (owner),
    INDEX idx_config_active (is_active),
    INDEX idx_config_updated_at (updated_at)
);

-- Configuration groups table
CREATE TABLE configuration_groups (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL UNIQUE,
    description TEXT,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Junction table for configuration groups
CREATE TABLE configuration_group_members (
    config_id UUID REFERENCES configurations(id) ON DELETE CASCADE,
    group_id UUID REFERENCES configuration_groups(id) ON DELETE CASCADE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    PRIMARY KEY (config_id, group_id),
    INDEX idx_group_member_config (config_id),
    INDEX idx_group_member_group (group_id)
);

-- Configuration history table
CREATE TABLE configuration_history (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    config_id UUID REFERENCES configurations(id) ON DELETE CASCADE,
    old_value TEXT,
    new_value TEXT,
    changed_by VARCHAR(255),
    changed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    INDEX idx_history_config (config_id),
    INDEX idx_history_changed_at (changed_at)
);

Primary Key Strategy

UUID is selected as the primary key for all tables due to:

  1. Global uniqueness - No coordination required across distributed systems
  2. Scalability - No contention on ID generation in high-concurrency environments
  3. Security - Prevents enumeration attacks by not exposing sequential patterns
  4. Cross-system compatibility - Easier to merge or replicate data across systems

Index Design

Index NameFieldsTypePurpose
idx_config_namenameStandardFast lookups by configuration name
idx_config_environmentenvironmentStandardEfficient filtering by environment
idx_config_ownerownerStandardPermission-based queries
idx_config_activeis_activeStandardFast filtering of active configurations
idx_config_updated_atupdated_atStandardEfficient change tracking
idx_group_member_configconfig_idStandardFast group membership lookups
idx_group_member_groupgroup_idStandardFast configuration-to-group queries
idx_history_configconfig_idStandardAudit trail lookups
idx_history_changed_atchanged_atStandardTime-based history queries

Data Partitioning / Sharding Strategy

For horizontal scaling, configurations will be sharded by:

  • Environment: dev, staging, prod - each gets dedicated shards
  • Owner: For high-volume owners, can shard by owner prefix (e.g., user_, service_)
  • Configuration type: JSON configs vs. simple values can be stored in separate tables

Soft Delete vs. Hard Delete Decision

Soft delete is chosen because:

  1. Audit trail preservation - History of configuration changes must be maintained
  2. Recovery capability - Ability to restore accidentally deleted configurations
  3. Compliance requirements - Regulatory compliance often requires data retention
  4. Referential integrity - Maintains foreign key relationships in history tables

Audit Trail Design

Each configuration change creates an audit record in configuration_history:

  • config_id: References the changed configuration
  • old_value: Previous value before change
  • new_value: New value after change
  • changed_by: User or service that made the change
  • changed_at: Timestamp of the change

Data Archival Strategy and Retention Policy

  1. Active configurations: Keep in primary storage indefinitely
  2. Historical data:
    • 30 days: Keep in primary storage for quick access
    • 90 days: Move to cold storage (e.g., S3 Glacier)
    • 1 year+: Archive to long-term storage with reduced access frequency
  3. Retention policy:
    • 5 years: Keep all configuration history for compliance
    • 10 years: Keep audit trail data for legal requirements
// Example configuration entity in Java
public class Configuration {
    private String id;
    private String name;
    private String description;
    private String value;
    private String type;
    private String environment;
    private String owner;
    private boolean isActive;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    private LocalDateTime deletedAt;
    
    // Getters and setters
}

4. Class Design & Service Layer

classDiagram
    class ConfigService {
        +getConfig(key: String, environment: String) : ConfigValue
        +updateConfig(key: String, value: String, environment: String) : boolean
        +deleteConfig(key: String, environment: String) : boolean
        +listConfigs(environment: String) : List~ConfigValue~
    }
    
    class ConfigRepository {
        +save(config: ConfigEntity) : void
        +findById(id: String) : Optional~ConfigEntity~
        +findByKeyAndEnvironment(key: String, environment: String) : Optional~ConfigEntity~
        +findAllByEnvironment(environment: String) : List~ConfigEntity~
        +deleteById(id: String) : void
    }
    
    class ConfigValue {
        -key: String
        -value: String
        -environment: String
        -version: Long
        -createdAt: Instant
        -updatedAt: Instant
        +getKey() : String
        +getValue() : String
        +getEnvironment() : String
        +getVersion() : Long
    }
    
    class ConfigEntity {
        -id: String
        -key: String
        -value: String
        -environment: String
        -version: Long
        -createdAt: Instant
        -updatedAt: Instant
        -deletedAt: Instant
        +getId() : String
        +getKey() : String
        +getValue() : String
        +getEnvironment() : String
        +getVersion() : Long
        +getCreatedAt() : Instant
        +getUpdatedAt() : Instant
        +isDeleted() : boolean
    }
    
    class ConfigCache {
        -cache: Cache~String, ConfigValue~
        +get(key: String, environment: String) : Optional~ConfigValue~
        +put(key: String, environment: String, value: ConfigValue) : void
        +invalidate(key: String, environment: String) : void
        +invalidateAll(environment: String) : void
    }
    
    class ConfigEventPublisher {
        +publish(event: ConfigChangeEvent) : void
    }
    
    class ConfigChangeEvent {
        -key: String
        -environment: String
        -oldValue: String
        -newValue: String
        -timestamp: Instant
    }
    
    class ConfigValidator {
        +validate(key: String, value: String) : ValidationResult
    }
    
    class ConfigManager {
        -configService: ConfigService
        -configCache: ConfigCache
        -configValidator: ConfigValidator
        -eventPublisher: ConfigEventPublisher
        +getConfig(key: String, environment: String) : ConfigValue
        +updateConfig(key: String, value: String, environment: String) : boolean
        +deleteConfig(key: String, environment: String) : boolean
        +reloadAll(environment: String) : void
    }
    
    ConfigService --> ConfigRepository : uses
    ConfigService --> ConfigCache : uses
    ConfigService --> ConfigEventPublisher : publishes
    ConfigService --> ConfigValidator : validates
    
    ConfigManager --> ConfigService : delegates to
    ConfigManager --> ConfigCache : uses for caching
    
    ConfigRepository ..|> Repository : implements
    ConfigService ..|> Service : implements
stateDiagram-v2
    [*] --> Active
    Active --> Deleted : deleteConfig()
    Active --> Updated : updateConfig()
    Deleted --> Active : restoreConfig()
    Updated --> Active : updateConfig()

Service Class Structure and Responsibilities

The configuration framework consists of several key service components:

  1. ConfigService: Core business logic for configuration operations
  2. ConfigManager: High-level orchestrator that handles caching, validation, and event publishing
  3. ConfigRepository: Data access layer for persistence operations
  4. ConfigCache: Caching layer to reduce database hits
  5. ConfigEventPublisher: Event publishing for configuration changes
  6. ConfigValidator: Input validation logic

Interface / Abstract Class Design

public interface Repository<T> {
    void save(T entity);
    Optional<T> findById(String id);
    void deleteById(String id);
}

public interface Service<T> {
    T create(T entity);
    T update(T entity);
    void delete(String id);
}

public interface ConfigValidator {
    ValidationResult validate(String key, String value);
}

Business Logic Encapsulation Patterns

Strategy Pattern for Validation: The framework supports different validation strategies based on configuration key types.

public class ConfigValidator {
    private final Map<String, ValidationStrategy> strategies = new HashMap<>();
    
    public ConfigValidator() {
        strategies.put("database", new DatabaseValidationStrategy());
        strategies.put("cache", new CacheValidationStrategy());
        strategies.put("api", new ApiValidationStrategy());
    }
    
    public ValidationResult validate(String key, String value) {
        ValidationStrategy strategy = strategies.getOrDefault(
            extractType(key), new DefaultValidationStrategy());
        return strategy.validate(key, value);
    }
    
    private String extractType(String key) {
        return key.split("\\.")[0];
    }
}

interface ValidationStrategy {
    ValidationResult validate(String key, String value);
}

class DatabaseValidationStrategy implements ValidationStrategy {
    @Override
    public ValidationResult validate(String key, String value) {
        if (value == null || value.isEmpty()) {
            return ValidationResult.failure("Database connection string cannot be empty");
        }
        if (!value.startsWith("jdbc:")) {
            return ValidationResult.failure("Invalid database URL format");
        }
        return ValidationResult.success();
    }
}

class DefaultValidationStrategy implements ValidationStrategy {
    @Override
    public ValidationResult validate(String key, String value) {
        return ValidationResult.success();
    }
}

Critical-Path Method Implementation

public class ConfigService {
    private final ConfigRepository repository;
    private final ConfigCache cache;
    private final ConfigEventPublisher eventPublisher;
    private final ConfigValidator validator;
    
    public boolean updateConfig(String key, String value, String environment) {
        // Transaction boundary - all operations within same DB transaction
        return transactionTemplate.execute(status -> {
            try {
                // Validate input
                ValidationResult validation = validator.validate(key, value);
                if (!validation.isSuccess()) {
                    throw new InvalidConfigException(
                        "Validation failed: " + validation.getMessage());
                }
                
                // Get current config or create new one
                ConfigEntity existing = repository.findByKeyAndEnvironment(key, environment)
                    .orElseGet(() -> new ConfigEntity());
                
                // Optimistic locking - check version before update
                Long currentVersion = existing.getVersion() != null ? existing.getVersion() : 0L;
                long newVersion = currentVersion + 1;
                
                // Prepare updated config
                ConfigEntity updated = new ConfigEntity();
                updated.setId(existing.getId() != null ? existing.getId() : UUID.randomUUID().toString());
                updated.setKey(key);
                updated.setValue(value);
                updated.setEnvironment(environment);
                updated.setVersion(newVersion);
                updated.setUpdatedAt(Instant.now());
                
                // Save to database
                repository.save(updated);
                
                // Update cache
                cache.put(key, environment, new ConfigValue(
                    key, value, environment, newVersion, 
                    updated.getCreatedAt(), updated.getUpdatedAt()));
                
                // Publish event
                ConfigChangeEvent event = new ConfigChangeEvent(
                    key, environment, existing.getValue(), value, Instant.now());
                eventPublisher.publish(event);
                
                return true;
            } catch (Exception e) {
                status.setRollbackOnly();
                throw e;
            }
        });
    }
}

Transaction Boundary Design

All configuration operations are wrapped in database transactions to ensure atomicity:

  • Create/Update/Delete operations are all within single transaction
  • Version checking prevents concurrent modification conflicts
  • Rollback on validation or persistence failures

Service Interface Contracts

public interface ConfigService {
    ConfigValue getConfig(String key, String environment);
    boolean updateConfig(String key, String value, String environment);
    boolean deleteConfig(String key, String environment);
    List<ConfigValue> listConfigs(String environment);
}

Saga / Orchestration Pattern

For complex configuration updates involving external systems:

  • Configuration changes that trigger external service reloads
  • Multi-step update operations (e.g., DB config + cache invalidation)
  • Event-driven orchestration using outbox pattern

Domain Object / Value Object Design

public class ConfigValue {
    private final String key;
    private final String value;
    private final String environment;
    private final Long version;
    private final Instant createdAt;
    private final Instant updatedAt;
    
    public ConfigValue(String key, String value, String environment, 
                      Long version, Instant createdAt, Instant updatedAt) {
        this.key = key;
        this.value = value;
        this.environment = environment;
        this.version = version;
        this.createdAt = createdAt;
        this.updatedAt = updatedAt;
    }
    
    // Getters only - immutable value object
}

public class ConfigEntity {
    private String id;
    private String key;
    private String value;
    private String environment;
    private Long version;
    private Instant createdAt;
    private Instant updatedAt;
    private Instant deletedAt;
    
    // Getters and setters for persistence
}

5. Concurrency & Thread Safety

Concurrency Model

The configuration framework will leverage a thread-safe, asynchronous processing model using Java's CompletableFuture and ExecutorService for handling concurrent configuration updates and fetches. The system will use a fixed thread pool with size tuned based on expected concurrent load, typically 16-32 threads for moderate to high throughput scenarios.

Race Condition Prevention Strategy

Race conditions in configuration updates will be prevented through:

  1. Atomic Updates: Using java.util.concurrent.atomic.AtomicReference for configuration snapshots to ensure atomic read-modify-write operations
  2. Version-based Conflict Detection: Each configuration has a version number that prevents concurrent writes from overwriting each other
  3. Read-Write Locks: For read-heavy workloads, a ReentrantReadWriteLock will be used to allow concurrent reads while ensuring exclusive writes

Optimistic vs. Pessimistic Locking Selection

Optimistic locking is selected for the configuration framework because:

  • Configuration updates are infrequent compared to reads
  • The system handles concurrent reads efficiently with caching
  • Version-based conflict detection is more scalable than database-level locking
  • Reduces contention and improves throughput under moderate concurrent load

Deadlock Prevention Strategy

Deadlock prevention will be implemented through:

  1. Lock Ordering: All configuration operations acquire locks in a consistent order (e.g., by namespace, then key)
  2. Timeout-based Lock Acquisition: Using tryLock() with timeout to prevent indefinite blocking
  3. Hierarchical Locking: Nested configuration operations will use a consistent lock hierarchy to prevent circular waits

Idempotency Implementation for Concurrent Duplicate Requests

Idempotency is handled through:

  1. Request Deduplication: Using a distributed cache (Redis) to track recent request IDs with TTL of 5 minutes
  2. Idempotency Key Strategy: For configuration updates, the client can provide an idempotencyKey in the request header
  3. State Consistency: Configuration updates are idempotent by design - applying the same configuration twice results in the same state
public class ConfigurationService {
    private final Map<String, AtomicReference<Configuration>> configurations = new ConcurrentHashMap<>();
    private final DistributedCache cache = new RedisDistributedCache();
    
    public CompletableFuture<Configuration> updateConfiguration(
            String namespace, 
            String key, 
            ConfigurationUpdateRequest request) {
        
        return CompletableFuture.supplyAsync(() -> {
            String idempotencyKey = request.getIdempotencyKey();
            
            // Check if this operation has been processed before
            if (idempotencyKey != null && cache.exists(idempotencyKey)) {
                return cache.get(idempotencyKey, Configuration.class);
            }
            
            // Perform optimistic update with version checking
            Configuration current = configurations.get(namespace + ":" + key).get();
            if (current.getVersion() != request.getExpectedVersion()) {
                throw new ConcurrentModificationException("Configuration has been modified");
            }
            
            // Update operation
            Configuration updated = current.withUpdates(request.getUpdates());
            configurations.get(namespace + ":" + key).set(updated);
            
            // Cache the result for idempotency
            if (idempotencyKey != null) {
                cache.set(idempotencyKey, updated, Duration.ofMinutes(5));
            }
            
            return updated;
        });
    }
}

Thread Safety in Configuration Loading

Configuration loading operations will be made thread-safe using:

  1. Double-Checked Locking Pattern for lazy initialization of configuration stores
  2. Immutable Configuration Objects to prevent modification after creation
  3. Thread-Local Caching for frequently accessed configurations in high-concurrency scenarios

Lock Granularity and Performance Trade-offs

  • Fine-grained locking at namespace/key level for maximum concurrency
  • Coarse-grained locking for batch operations to reduce lock contention
  • Read-Write separation where read-heavy workloads benefit from concurrent readers

The design balances between maximizing throughput and maintaining data consistency under high concurrent loads, with optimistic locking providing the best performance characteristics for typical configuration update patterns.

6. Sequence Diagrams for Key Flows

sequenceDiagram
    participant Client
    participant ConfigService
    participant Cache
    participant Database

    Client->>ConfigService: GET /config/app1/featureA
    ConfigService->>Cache: Check if config exists in cache
    Cache-->>ConfigService: Config found (hit)
    ConfigService-->>Client: Return cached config
sequenceDiagram
    participant Client
    participant ConfigService
    participant Cache
    participant Database

    Client->>ConfigService: GET /config/app1/featureA
    ConfigService->>Cache: Check if config exists in cache
    Cache-->>ConfigService: Config not found (miss)
    ConfigService->>Database: Fetch config from database
    Database-->>ConfigService: Return config
    ConfigService->>Cache: Store config in cache
    Cache-->>ConfigService: Cache set success
    ConfigService-->>Client: Return config
sequenceDiagram
    participant Client
    participant ConfigService
    participant EventPublisher
    participant ConfigChangeConsumer

    Client->>ConfigService: PUT /config/app1/featureA
    ConfigService->>Database: Update config in database
    Database-->>ConfigService: Update success
    ConfigService->>EventPublisher: Publish config.change event
    EventPublisher-->>ConfigChangeConsumer: Deliver event to consumers
    ConfigChangeConsumer->>ConfigChangeConsumer: Invalidate local cache
    ConfigChangeConsumer-->>ConfigChangeConsumer: Refresh config from source

7. Caching Design

Cache Strategy Selection

For the configuration framework, we'll use a cache-aside pattern with write-through behavior for critical configuration updates. This approach provides the best balance between consistency and performance for a configuration system where data freshness is paramount.

The cache-aside pattern allows us to:

  • Load configuration from cache first, falling back to database
  • Explicitly control when and how data is cached
  • Handle cache misses gracefully without blocking operations

Cache Key Design

Cache keys will follow a structured namespace pattern:

config:{environment}:{config_key}:{version}
config:metadata:{environment}:{config_key}
config:watchers:{environment}:{config_key}

Example key generation:

public class ConfigCacheKey {
    private final String environment;
    private final String configKey;
    private final String version;
    
    public String generateKey() {
        return String.format("config:%s:%s:%s", 
                           environment, configKey, version);
    }
    
    public String generateMetadataKey() {
        return String.format("config:metadata:%s:%s", 
                           environment, configKey);
    }
}

TTL Configuration

Data TypeTTLReason
Configuration values5 minutesFrequent updates may require quick propagation
Metadata (last_modified, version)1 hourLess volatile than actual config values
Watchers list30 minutesConfiguration change notifications are less frequent

Cache Eviction Policy

We'll use LRU (Least Recently Used) eviction policy with a secondary TTL-based cleanup:

  • Primary eviction based on access patterns to prevent memory bloat
  • Secondary cleanup for stale entries that exceed TTL limits

Cache Stampede Prevention

To prevent cache stampedes:

  1. Probabilistic early expiry: Set cache TTL to 75% of configured value
  2. Distributed locks: Use Redis distributed locks for cache population
  3. Cache warming: Pre-warm cache during system startup
public class CacheStampedeProtection {
    private final RedisTemplate<String, Object> redisTemplate;
    
    public boolean acquireLock(String key, String lockValue, int ttlSeconds) {
        return redisTemplate.opsForValue()
            .setIfAbsent(key + ":lock", lockValue, Duration.ofSeconds(ttlSeconds));
    }
    
    public void releaseLock(String key, String lockValue) {
        String lockKey = key + ":lock";
        String currentValue = (String) redisTemplate.opsForValue().get(lockKey);
        if (lockValue.equals(currentValue)) {
            redisTemplate.delete(lockKey);
        }
    }
}

Distributed Cache Architecture

We'll implement a Redis cluster with:

  • 3 master nodes for high availability
  • 3 replica nodes per master for read scaling
  • Redis Sentinel for automatic failover
  • Cache sharding based on environment to avoid hotspots

Cache Invalidation Strategy

We'll use event-driven invalidation with the following approach:

  1. Write-through: When configuration is updated, invalidate cache entries
  2. Version-based invalidation: Update version metadata and trigger cache removal
  3. Explicit purging: Support for manual cache clearing via admin API
@Component
public class ConfigCacheInvalidator {
    
    @EventListener
    public void handleConfigUpdate(ConfigUpdatedEvent event) {
        String cacheKey = generateCacheKey(event.getEnvironment(), event.getConfigKey());
        String metadataKey = generateMetadataKey(event.getEnvironment(), event.getConfigKey());
        
        // Remove from cache
        redisTemplate.delete(cacheKey);
        redisTemplate.delete(metadataKey);
        
        // Notify watchers via pub/sub
        publishWatchEvent(event.getEnvironment(), event.getConfigKey());
    }
}

Cache Warm-up Strategy

To minimize cold starts:

  1. Startup warm-up: Load frequently accessed configurations during application startup
  2. Background warming: Warm cache for recently accessed keys in background
  3. Pre-warming: Load configuration based on usage patterns from monitoring
@Component
public class ConfigCacheWarmer {
    
    @EventListener
    public void warmCacheOnStartup() {
        // Warm up most frequently accessed configurations
        List<String> popularConfigs = getPopularConfigKeys();
        
        popularConfigs.forEach(key -> {
            try {
                // Load and cache configurations
                loadAndCacheConfig(key);
            } catch (Exception e) {
                // Log but don't fail startup
                logger.warn("Failed to warm cache for key: {}", key, e);
            }
        });
    }
}

8. Repository / Data Access Layer

Repository Pattern Implementation

The repository pattern is implemented to abstract data access logic and provide a clean separation between domain objects and persistence concerns. Each configuration entity has its own repository interface with standardized CRUD operations.

public interface ConfigurationRepository {
    Optional<Configuration> findById(String id);
    List<Configuration> findAll();
    List<Configuration> findByEnvironmentAndKey(String environment, String key);
    Configuration save(Configuration config);
    void deleteById(String id);
    boolean existsById(String id);
}

public interface ConfigurationHistoryRepository {
    List<ConfigurationHistory> findByConfigId(String configId);
    void save(ConfigurationHistory history);
}

ORM vs. Raw Query Decisions

For the configuration framework, we use a hybrid approach:

  • ORM for simple CRUD operations: Hibernate/JPA for basic find/save/delete operations to reduce boilerplate
  • Raw SQL for complex queries: Custom queries for configuration history and versioning operations to ensure optimal performance
@Repository
public class ConfigurationRepositoryImpl implements ConfigurationRepository {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    @Override
    public Optional<Configuration> findById(String id) {
        return Optional.ofNullable(
            entityManager.find(Configuration.class, id)
        );
    }
    
    @Override
    public Configuration save(Configuration config) {
        if (config.getId() == null) {
            entityManager.persist(config);
        } else {
            entityManager.merge(config);
        }
        return config;
    }
    
    @Override
    public List<Configuration> findByEnvironmentAndKey(String environment, String key) {
        return entityManager.createQuery(
            "SELECT c FROM Configuration c WHERE c.environment = :env AND c.key = :key",
            Configuration.class)
            .setParameter("env", environment)
            .setParameter("key", key)
            .getResultList();
    }
}

Read vs. Write Separation

We implement CQRS pattern for the configuration framework to optimize read and write operations:

public class ConfigurationReadModel {
    private String id;
    private String key;
    private String environment;
    private String value;
    private Long version;
    private Instant lastModified;
    
    // Getters and setters
}

@Repository
public class ConfigurationReadRepository {
    private final JdbcTemplate jdbcTemplate;
    
    public List<ConfigurationReadModel> findByEnvironment(String environment) {
        return jdbcTemplate.query(
            "SELECT id, key, environment, value, version, last_modified FROM config_read_view WHERE environment = ?",
            new Object[]{environment},
            (rs, rowNum) -> new ConfigurationReadModel(
                rs.getString("id"),
                rs.getString("key"),
                rs.getString("environment"),
                rs.getString("value"),
                rs.getLong("version"),
                rs.getTimestamp("last_modified").toInstant()
            )
        );
    }
}

Connection Pool Configuration

Connection pool is configured with:

  • Maximum pool size: 20
  • Minimum idle connections: 5
  • Connection timeout: 30 seconds
  • Validation query: SELECT 1
spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      validation-timeout: 5000
      idle-timeout: 600000
      max-lifetime: 1800000

N+1 Query Prevention Strategy

To prevent N+1 queries, we implement eager loading for configuration history and use batch operations:

@Repository
public class ConfigurationRepositoryImpl implements ConfigurationRepository {
    
    @Override
    public List<Configuration> findAllWithHistory() {
        // Using JOIN FETCH to eagerly load history
        return entityManager.createQuery(
            "SELECT c FROM Configuration c LEFT JOIN FETCH c.history h ORDER BY c.lastModified DESC",
            Configuration.class)
            .getResultList();
    }
    
    @Transactional
    public void batchSave(List<Configuration> configs) {
        for (int i = 0; i < configs.size(); i++) {
            entityManager.persist(configs.get(i));
            if (i % 50 == 0) {
                entityManager.flush();
                entityManager.clear();
            }
        }
    }
}

Batch Operation Design

Batch operations are implemented for bulk configuration updates:

@Repository
public class ConfigurationBatchRepository {
    
    public void batchUpdate(List<ConfigurationUpdateRequest> updates) {
        String sql = "UPDATE configuration SET value = ?, version = version + 1, last_modified = ? WHERE id = ?";
        
        try (Connection conn = dataSource.getConnection();
             PreparedStatement stmt = conn.prepareStatement(sql)) {
            
            for (ConfigurationUpdateRequest update : updates) {
                stmt.setString(1, update.getValue());
                stmt.setTimestamp(2, Timestamp.from(Instant.now()));
                stmt.setString(3, update.getId());
                stmt.addBatch();
            }
            
            stmt.executeBatch();
        } catch (SQLException e) {
            throw new DataAccessException("Failed to batch update configurations", e);
        }
    }
}

9. Asynchronous Processing & Messaging

Event-driven boundaries in this feature

The configuration framework will use a hybrid synchronous/asynchronous approach. The core configuration update operations (create, update, delete) will be handled synchronously to ensure immediate consistency for the calling service. However, configuration changes will trigger asynchronous events to notify downstream services that depend on these configurations.

The primary async boundary is:

  • Configuration change → Event publishing (to notify subscribers)
  • Configuration validation → Async validation job (for complex validation logic)
  • Configuration audit logging → Async audit processing (to avoid blocking primary operations)

Message schema design with sample payload

{
  "version": "1.0",
  "event_type": "configuration.updated",
  "timestamp": "2023-10-15T14:30:00Z",
  "source": "config-service",
  "data": {
    "config_id": "cfg-12345",
    "key": "feature.flag.new-ui",
    "value": "true",
    "environment": "production",
    "version": 2,
    "previous_value": "false"
  },
  "metadata": {
    "trace_id": "abc-123-def-456",
    "correlation_id": "corr-789"
  }
}

Dead letter queue strategy and poison message handling

All configuration events will be published to a dead-letter queue (DLQ) when:

  1. Message processing fails more than 3 times
  2. Message is malformed or cannot be deserialized
  3. Consumer experiences a system-level failure

Poison messages will be:

  • Automatically moved to DLQ after 3 failed attempts
  • Logged with detailed error context for debugging
  • Made available for manual reprocessing via admin interface
  • Tracked with metrics to identify problematic patterns

At-least-once vs. exactly-once delivery

At-least-once delivery is selected for configuration events because:

  • Configuration changes are idempotent operations
  • Duplicate processing won't cause data corruption or business logic errors
  • The system can handle duplicate events gracefully through versioning and idempotency keys

The system uses message deduplication via unique event IDs to prevent processing the same configuration change multiple times.

Retry policy for consumers

Retry policy:

  • Maximum 3 attempts with exponential backoff (1s, 2s, 4s)
  • Jitter added to prevent thundering herd
  • Non-retryable errors (malformed messages, permission denied) will not be retried
  • Retry logic implemented using circuit breaker pattern

Outbox pattern for reliable event publishing

The outbox pattern will be implemented to ensure configuration events are reliably published:

public class ConfigurationEventOutbox {
    private final EventPublisher eventPublisher;
    private final ConfigurationEventRepository eventRepository;

    public void publishConfigurationEvent(ConfigurationEvent event) {
        // 1. Save event to outbox table
        ConfigurationEventOutboxEntry entry = new ConfigurationEventOutboxEntry(event);
        eventRepository.save(entry);

        // 2. Publish to message broker
        try {
            eventPublisher.publish(entry.getEvent());
            // 3. Mark as published in outbox
            entry.markAsPublished();
            eventRepository.update(entry);
        } catch (Exception e) {
            // 4. Handle publish failure - will be retried by background job
            entry.markAsFailed();
            eventRepository.update(entry);
        }
    }
}

Sequence diagram for async flows

sequenceDiagram
    participant Service
    participant OutboxRepo
    participant MessageBroker
    participant ConsumerA
    participant ConsumerB

    Service->>OutboxRepo: Save event to outbox
    OutboxRepo-->>Service: Acknowledge save
    Service->>MessageBroker: Publish event to topic
    MessageBroker-->>ConsumerA: Deliver event
    MessageBroker-->>ConsumerB: Deliver event
    ConsumerA->>ConsumerA: Process configuration update
    ConsumerB->>ConsumerB: Validate and cache config

10. Error Handling Design

Error Classification Hierarchy

The configuration framework implements a three-level error classification system:

public abstract class ConfigException extends RuntimeException {
    public ConfigException(String message) { super(message); }
    public ConfigException(String message, Throwable cause) { super(message, cause); }
}

public class ConfigValidationException extends ConfigException {
    public ConfigValidationException(String message) { super(message); }
}

public class ConfigNotFoundException extends ConfigException {
    public ConfigNotFoundException(String message) { super(message); }
}

public class ConfigPersistenceException extends ConfigException {
    public ConfigPersistenceException(String message, Throwable cause) { super(message, cause); }
}

public class ConfigServiceException extends ConfigException {
    public ConfigServiceException(String message, Throwable cause) { super(message, cause); }
}

Error Propagation Strategy

Errors are handled at the appropriate layer with specific propagation patterns:

  1. Validation errors (ConfigValidationException) are caught and converted to HTTP 400 responses at the API layer
  2. Business logic errors (ConfigNotFoundException) are handled by service layer and converted to appropriate HTTP responses
  3. Infrastructure errors (ConfigPersistenceException) are wrapped and re-thrown to service layer for circuit breaker handling
  4. System errors (ConfigServiceException) are logged and returned as 500 responses

Retry Strategy with Exponential Backoff

Retry logic is implemented using exponential backoff with jitter to prevent thundering herd:

public class RetryConfig {
    private static final int MAX_RETRIES = 3;
    private static final long BASE_DELAY_MS = 1000;
    private static final double JITTER_FACTOR = 0.2;
    
    public static long calculateDelay(int attempt) {
        long delay = (long) (BASE_DELAY_MS * Math.pow(2, attempt));
        long jitter = (long) (delay * JITTER_FACTOR * (Math.random() - 0.5) * 2);
        return Math.min(delay + jitter, 30000); // Cap at 30 seconds
    }
    
    public static boolean shouldRetry(int attempt, Throwable error) {
        return attempt < MAX_RETRIES && 
               (error instanceof ConfigPersistenceException || 
                error instanceof ConfigServiceException);
    }
}

Circuit Breaker Design

The circuit breaker pattern prevents cascading failures during infrastructure outages:

public class CircuitBreaker {
    private final String name;
    private volatile CircuitState state = CircuitState.CLOSED;
    private int failureCount = 0;
    private long lastFailureTime = 0;
    private final int failureThreshold = 5;
    private final long timeoutMs = 60000; // 1 minute
    
    public enum CircuitState {
        CLOSED, OPEN, HALF_OPEN
    }
    
    public boolean allowRequest() {
        switch (state) {
            case CLOSED:
                return true;
            case OPEN:
                if (System.currentTimeMillis() - lastFailureTime > timeoutMs) {
                    state = CircuitState.HALF_OPEN;
                    return true;
                }
                return false;
            case HALF_OPEN:
                return true;
        }
        return true;
    }
    
    public void recordSuccess() {
        if (state == CircuitState.HALF_OPEN) {
            state = CircuitState.CLOSED;
            failureCount = 0;
        }
    }
    
    public void recordFailure() {
        failureCount++;
        lastFailureTime = System.currentTimeMillis();
        
        if (failureCount >= failureThreshold) {
            state = CircuitState.OPEN;
        }
    }
}

Fallback Behavior Design

Fallbacks are implemented for critical dependencies:

public class ConfigFallbackHandler {
    private final Map<String, Object> defaultConfig = new HashMap<>();
    
    public ConfigValue getFallback(String key) {
        return Optional.ofNullable(defaultConfig.get(key))
                      .map(value -> new ConfigValue(key, value.toString()))
                      .orElseThrow(() -> new ConfigNotFoundException("Config not found: " + key));
    }
    
    public List<ConfigValue> getFallbackList(String namespace) {
        return Collections.emptyList(); // Return empty list instead of throwing
    }
}

Error Logging Format

Structured logging follows a consistent format:

public class ConfigErrorLogger {
    private static final Logger logger = LoggerFactory.getLogger(ConfigErrorLogger.class);
    
    public void logError(String operation, String errorType, String errorMessage, 
                        Map<String, Object> context) {
        logger.error("CONFIG_ERROR",
            "operation", operation,
            "error_type", errorType,
            "message", errorMessage,
            "context", context,
            "timestamp", System.currentTimeMillis(),
            "trace_id", MDC.get("trace_id"));
    }
}

Alerting Thresholds

Critical error thresholds trigger alerts:

Error TypeAlert ThresholdSeverity
ConfigPersistenceException5 errors/minuteCRITICAL
ConfigServiceException10 errors/minuteMAJOR
ConfigNotFoundException50 errors/minuteWARNING

Error Response Format

Standardized JSON error responses:

{
  "error": {
    "code": "CONFIG_NOT_FOUND",
    "message": "Configuration key 'database.url' not found",
    "timestamp": "2023-10-15T14:30:00Z",
    "request_id": "req-12345",
    "details": {
      "key": "database.url",
      "namespace": "production"
    }
  }
}

11. Security Design

Input Validation and Sanitization Strategy

All configuration inputs undergo strict validation using a combination of:

  • Regex patterns for format validation (e.g., IP addresses, email formats)
  • Length limits to prevent buffer overflow attacks
  • Type checking to ensure proper data types (string, integer, boolean)
  • Whitelisting for allowed configuration keys and values
  • Custom validators for complex structures (e.g., JSON schema validation for nested configs)
public class ConfigValidator {
    private static final Pattern IP_PATTERN = Pattern.compile(
        "^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$");
    
    public static boolean isValidIpAddress(String ip) {
        return IP_PATTERN.matcher(ip).matches();
    }
    
    public static boolean isValidConfigKey(String key) {
        return key != null && 
               key.length() <= 255 && 
               key.matches("^[a-zA-Z0-9_\\-\\.]+$");
    }
}

SQL Injection Prevention

All database queries use parameterized statements with prepared statements to prevent injection attacks. No dynamic query building or string concatenation is allowed for configuration data.

Authentication Middleware Design

Authentication middleware intercepts all requests and validates:

  • JWT tokens for API access (with audience, issuer, and expiration checks)
  • API keys for service-to-service communication
  • OAuth2 tokens for user-based access (when applicable)
@Component
public class AuthMiddleware {
    private final JwtService jwtService;
    
    public void validateRequest(HttpServletRequest request) {
        String authHeader = request.getHeader("Authorization");
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            throw new SecurityException("Missing or invalid auth token");
        }
        
        String token = authHeader.substring(7);
        if (!jwtService.validateToken(token)) {
            throw new SecurityException("Invalid or expired token");
        }
    }
}

Authorization Enforcement (RBAC)

Role-based access control is enforced at service level:

  • Admin role - Full CRUD access to all configurations
  • Developer role - Read/write access to their application's configurations
  • Viewer role - Read-only access to configurations
@PreAuthorize("hasRole('ADMIN') or hasPermission(#appId, 'CONFIG_WRITE')")
public void updateConfiguration(String appId, ConfigUpdateRequest request) {
    // Implementation
}

Sensitive Data Encryption at Rest and in Transit

  • At rest: AES-256 encryption using AWS KMS or similar key management service
  • In transit: TLS 1.3 with HSTS header enforcement (minimum 2048-bit RSA keys)
  • Configuration values: Sensitive fields (passwords, tokens) are encrypted before storage

PII Handling and Masking Strategy

  • PII fields in configuration (e.g., email addresses, phone numbers) are masked in logs and responses
  • Masking pattern: user@example.com → u***@example.com
  • Log filtering: All sensitive fields are excluded from structured logs

Secrets Management

Configuration framework integrates with AWS Secrets Manager or HashiCorp Vault for:

  • Secure storage of sensitive configuration values (API keys, passwords)
  • Automatic rotation policies
  • Audit logging for secret access

Audit Logging Design

All configuration changes are logged with:

  • Event type: CREATE, UPDATE, DELETE, ACCESS
  • User/Service ID: Actor identifier
  • Timestamp: UTC with millisecond precision
  • Configuration details: Old and new values (masked for sensitive data)
  • IP address: Source of the request
@EventListener
public void handleConfigChange(ConfigChangeEvent event) {
    AuditLog auditLog = AuditLog.builder()
        .userId(event.getUserId())
        .action(event.getAction())
        .configId(event.getConfigId())
        .oldValue(maskSensitiveFields(event.getOldValue()))
        .newValue(maskSensitiveFields(event.getNewValue()))
        .timestamp(Instant.now())
        .ipAddress(getClientIpAddress())
        .build();
    
    auditLogRepository.save(auditLog);
}

12. Testing Strategy

Unit Testing Scope and Coverage Targets (%)

  • Service Layer: 85% code coverage, focusing on business logic and edge cases
  • Repository Layer: 90% coverage with all CRUD operations tested
  • Domain Objects: 100% coverage for value objects and immutable entities
  • Configuration Management: 95% coverage including validation, transformation, and fallback logic
  • Event Handlers: 80% coverage for async processing paths

Integration Testing Strategy

  • Database Integration: Test with embedded H2 in-memory database for fast test execution, using testcontainers for real DB testing in CI
  • External Service Mocks: Use WireMock or Mockito to simulate external configuration sources (e.g., Consul, etcd)
  • Event Broker Integration: Use embedded Kafka or RabbitMQ for async flow testing in test environments
  • Caching Layer: Integration tests with Redis mock or embedded Redis for cache behavior verification

Contract Testing

  • API Contracts: Use Pact framework to define and verify contracts between configuration service and consumers
  • Event Contracts: Define Avro schemas with versioning strategy and validate consumer-producer compatibility using Schema Registry
  • External Service Contracts: Test against real external configuration providers (Consul, etcd) in staging environments

Performance Testing Tool

  • Load Testing: Use k6 for load testing configuration service endpoints with concurrent requests simulating real-world usage
  • Key Scenarios:
    • High concurrent config fetch operations (1000+ concurrent requests)
    • Large configuration payloads (>5MB) with caching behavior
    • Configuration change propagation across service instances
  • Metrics Tracked:
    • p50/p95/p99 response times under load
    • Memory usage and GC pressure during high-concurrency scenarios
    • Cache hit ratio under various load patterns

Test Data Management Strategy

  • Fixtures: Use JSON/YAML fixtures for configuration templates and default values in test resources
  • Factories: Implement factory classes to generate valid configuration objects with various combinations of properties and overrides
  • Seeding: Use database seeding scripts for test data population, with cleanup procedures between test runs
  • Test Data Isolation: Each test suite runs with isolated database instances or schema namespaces to prevent cross-contamination
// Example unit test for configuration validation logic
@Test
public void testValidateConfigurationWithInvalidProperty() {
    Configuration config = new Configuration.Builder()
        .withKey("database.url")
        .withValue("invalid-url-format")
        .build();
    
    assertThatThrownBy(() -> configurationValidator.validate(config))
        .isInstanceOf(ValidationException.class)
        .hasMessageContaining("Invalid database URL format");
}

// Example integration test for cache invalidation
@Test
public void testConfigurationUpdateInvalidatesCache() {
    // Given: Cached configuration exists
    Configuration cachedConfig = configurationService.get("app.config");
    
    // When: Configuration is updated
    Configuration updatedConfig = new Configuration.Builder()
        .withKey("app.config")
        .withValue("new-value")
        .build();
    configurationService.update(updatedConfig);
    
    // Then: Cache is invalidated and fresh value returned
    Configuration freshConfig = configurationService.get("app.config");
    assertThat(freshConfig.getValue()).isEqualTo("new-value");
}

13. Observability Design

Structured Logging Schema

The logging system will follow a structured JSON format to ensure consistency and ease of parsing across services. The schema includes:

{
  "timestamp": "2023-10-27T10:00:00.000Z",
  "level": "INFO",
  "service": "config-service",
  "trace_id": "1234567890abcdef1234567890abcdef",
  "span_id": "abcdef1234567890abcdef1234567890",
  "user_id": "user-123",
  "entity_id": "config-abc123",
  "operation": "getConfig",
  "message": "Configuration retrieved successfully",
  "duration_ms": 15,
  "error_code": null,
  "error_message": null,
  "stack_trace": null,
  "tags": {
    "environment": "production",
    "version": "v1.2.3"
  }
}

PII masking rules:

  • User IDs and session tokens are masked in logs
  • Sensitive config values (passwords, secrets) are redacted with ***
  • IP addresses are truncated to network segment level (e.g., 192.168.1.***)

Distributed Tracing Instrumentation

Using OpenTelemetry, we'll instrument key operations with custom spans:

Key Operations to Trace:

  1. config.fetch - Fetching configuration from database or cache
  2. config.validate - Validating configuration schema and values
  3. config.publish - Publishing configuration updates to subscribers
  4. config.update - Updating configuration in database

Span Attributes Captured:

  • config.id: Unique identifier for the configuration
  • config.environment: Target environment (dev/staging/prod)
  • config.version: Version of the configuration
  • config.source: Source of config (DB, file, API)
  • config.update_type: Type of update (create, modify, delete)

Metrics Design

RED Method Metrics:

Metric NameTypeDescriptionLabels
config_requests_totalCounterTotal config requestsenvironment, operation, status
config_request_duration_secondsHistogramRequest processing timeenvironment, operation
config_errors_totalCounterConfig-related errorsenvironment, error_type, operation
config_cache_hit_ratioGaugeCache hit ratioenvironment, cache_type
config_updates_totalCounterConfiguration updatesenvironment, update_type

Additional Metrics:

  • config_active_subscribers (Gauge): Number of active subscribers
  • config_validation_failures_total (Counter): Failed config validations
  • config_cache_size (Gauge): Current cache size in bytes

Health Check Endpoint Design

Endpoints:

  1. /health - Overall service health (liveness + readiness)
  2. /ready - Service ready to serve requests
  3. /live - Service process is alive (basic liveness)
  4. /startup - Startup health check for initialization

Response Format:

{
  "status": "healthy",
  "timestamp": "2023-10-27T10:00:00.000Z",
  "checks": {
    "database": {
      "status": "healthy",
      "latency_ms": 5
    },
    "cache": {
      "status": "healthy",
      "latency_ms": 2
    },
    "dependencies": {
      "status": "healthy",
      "latency_ms": 10
    }
  },
  "version": "v1.2.3",
  "environment": "production"
}

Alerting Thresholds

Critical Alerts (Escalation to On-Call):

  • config_requests_total - Rate > 10k/sec for 5 minutes (warn: 5k/sec)
  • config_request_duration_seconds - p99 > 500ms (warn: 200ms)
  • config_errors_total - Rate > 10/sec for 1 minute (warn: 5/sec)
  • config_cache_hit_ratio - Below 80% for 10 minutes (warn: 90%)

Warning Alerts:

  • config_validation_failures_total - Rate > 1/sec for 5 minutes
  • config_updates_total - Rate < 1/sec for 1 hour (indicates potential downtime)
  • config_cache_size - Growth rate > 50MB/min for 10 minutes

Alerting Configuration:

  • Use Prometheus alerting rules with Slack/Email integration
  • Critical alerts trigger immediate on-call escalation
  • Warning alerts trigger notification to team Slack channel

14. Open Questions, Risks & Trade-offs

Decisions requiring product / engineering alignment

  • Configuration format support: Should we support only JSON/YAML or also support more complex formats like HCL, TOML, or environment variable interpolation? This decision impacts the complexity of our parser and the flexibility available to users.
  • Multi-environment configuration strategy: How should we handle configurations that vary by environment (dev, staging, prod)? Should we support hierarchical configuration inheritance or separate configuration sets per environment?
  • Configuration versioning strategy: Do we need to support versioned configurations for rollback capability? If so, what's the lifecycle management approach (e.g., soft delete vs. archival)?
  • Push vs. pull model: Should configuration updates be pushed to clients or pulled by them? Push introduces complexity in maintaining client connections but provides real-time updates.

Known trade-offs and justifications

Trade-offOption 1Option 2Justification
Consistency vs. AvailabilityStrong consistency with distributed locksEventual consistency with conflict resolutionFor high-availability systems, eventual consistency with conflict resolution (e.g., last-write-wins) provides better availability than strict consistency, which can cause blocking and reduce system responsiveness
Centralized vs. Distributed ConfigurationSingle centralized serviceDistributed configuration with client-side cachingCentralized provides better consistency and auditability but creates a single point of failure. Distributed approach offers better scalability and resilience but increases complexity in maintaining consistency
Real-time Updates vs. Batch RefreshReal-time push updatesPeriodic polling with cache invalidationReal-time updates provide better responsiveness but introduce complexity in managing client connections and handling network failures. Polling is simpler to implement but introduces latency
Configuration Validation vs. FlexibilityStrict validation with schema enforcementFlexible parsing with runtime error handlingStrict validation ensures configuration integrity but can be restrictive for complex or evolving systems. Flexible parsing allows more experimentation but increases risk of runtime errors

Risks and mitigation strategies

  1. Configuration drift: Different services may end up with different configuration values due to concurrent updates or cache invalidation issues.

    • Mitigation: Implement strict validation, audit trails, and periodic consistency checks across services.
  2. Performance degradation under high load: Configuration service may become a bottleneck during peak usage.

    • Mitigation: Implement robust caching strategy, horizontal scaling, and read replicas for frequently accessed configurations.
  3. Security vulnerabilities: Misconfigured permissions or insecure configuration storage could lead to unauthorized access.

    • Mitigation: Implement comprehensive RBAC, encryption at rest and in transit, and regular security audits.
  4. Race conditions during configuration updates: Concurrent updates may result in lost updates or inconsistent states.

    • Mitigation: Use optimistic locking with version numbers and implement proper retry logic for update operations.
  5. Caching inconsistency: Cached configurations may become stale, leading to services operating on outdated settings.

    • Mitigation: Implement TTL-based cache invalidation, use cache warming strategies, and provide manual refresh capabilities.

Technical debt acknowledged

  1. Parser complexity: Supporting multiple configuration formats increases code complexity and maintenance burden.
  2. Eventual consistency model: The eventual consistency approach introduces subtle bugs related to timing and state synchronization.
  3. Audit trail implementation: Comprehensive audit logging adds performance overhead and storage requirements.
  4. Cross-service configuration propagation: Changes to shared configurations may require coordination across multiple services, creating potential for partial updates or synchronization delays.
  5. Version conflict resolution: The current design assumes simple conflict resolution strategies (e.g., last-write-wins) which may not be suitable for complex configuration hierarchies where merge strategies are required.

Was this article helpful?

Topics in this article

More from Backend LLD

  • Backend LLD

    API Request Validation Framework Design

    Comprehensive low-level design for a robust, extensible API request validation framework with real-time validation and error handling capabilities.

  • Backend LLD

    Template Method Pattern Framework Design

    Comprehensive low-level design for a flexible template method pattern implementation with extensibility and performance considerations

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.