Skip to content

Primitive Obsession - Configuration uses primitive types requiring runtime validation #500

@nachog00

Description

@nachog00

Primitive Obsession - Configuration uses primitive types requiring runtime validation

Labels: configs, tech-debt, type-safety

Problem

The configuration system suffers from Primitive Obsession - using primitive types (strings, Option<usize>) where domain-specific types would be safer and more expressive.

Current Problematic Code

// IndexerConfig
pub struct IndexerConfig {
    pub network: String,  // Runtime validation needed!
    pub db_size: Option<usize>,  // What does None mean? Unlimited? Unset?
    pub map_shard_amount: Option<usize>,  // Panics if not power of 2!
    pub grpc_tls: bool,  // Boolean blindness
    pub tls_cert_path: Option<String>,  // Must validate these together
    pub tls_key_path: Option<String>,
    // ...
}

// Runtime validation required
impl IndexerConfig {
    pub fn check_config(&self) -> Result<(), IndexerError> {
        // String comparison for network - error prone!
        if (self.network != "Regtest") && 
           (self.network != "Testnet") && 
           (self.network != "Mainnet") {
            return Err(IndexerError::ConfigError(
                "Incorrect network name given...".to_string(),
            ));
        }
        
        // TLS validation - multiple fields must be checked together
        if self.grpc_tls {
            if self.tls_cert_path.is_none() {
                return Err(IndexerError::ConfigError(
                    "TLS enabled but no cert path".to_string(),
                ));
            }
            // More validation...
        }
    }
}

// DashMap will panic at runtime!
// Documentation: "shard_amount should be power of two or function will panic"
pub map_shard_amount: Option<usize>,

Impact

  • Runtime Errors: Invalid configs discovered only at runtime
  • Poor UX: Users see panics instead of clear config errors
  • Ambiguous Semantics: None can mean different things in different contexts
  • Validation Sprawl: Validation logic scattered throughout codebase
  • Type Confusion: Easy to pass wrong string or number

Solution

Use domain-specific types that make invalid states unrepresentable:

// Network as enum - compile-time safety
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Network {
    Mainnet,
    Testnet,
    Regtest,
}

// Explicit database size semantics
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub enum DatabaseSize {
    Unlimited,
    Gb(usize),  // Clear units!
    // Future: Mb(usize), Tb(usize)
}

// TLS config - impossible to have invalid state
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub enum TlsConfig {
    Disabled,
    Enabled {
        cert_path: PathBuf,  // Both required!
        key_path: PathBuf,
    },
}

// Power-of-two enforced at type level
pub struct CacheConfig {
    pub capacity: usize,
    pub shard_power: u8,  // 2^shard_power shards - can't be invalid!
}

impl CacheConfig {
    pub fn shard_count(&self) -> usize {
        1 << self.shard_power  // Always valid
    }
}

Configuration becomes self-validating:

// No runtime validation needed!
pub struct IndexerConfig {
    pub network: Network,  // Can only be valid variant
    pub database_size: DatabaseSize,  // Clear semantics
    pub tls: TlsConfig,  // Impossible to have TLS enabled without paths
    pub cache: Option<CacheConfig>,  // None means "use defaults"
}

// TOML becomes clearer too:
// network = "mainnet"  # Validated at parse time
// database_size = { gb = 10 }  # Or "unlimited"
// tls = { enabled = { cert_path = "...", key_path = "..." }}
// cache = { capacity = 1000, shard_power = 4 }  # 16 shards

Benefits:

  • Parse-time validation instead of runtime
  • Self-documenting configuration
  • Impossible to create invalid states
  • No runtime panics from bad config
  • Better IDE autocomplete and documentation

Metadata

Metadata

Assignees

Labels

ConfigsRelated to overall configuration of zainotech-debtTechnical debt that needs to be addressedtype-safetyType safety improvements

Type

No type

Projects

Status

In progress

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions