Back to Blog

Configuration Management: YAML to Runtime Config Chain

In a platform composed of three processes, configuration management appears simple but is actually the epicenter of operational pain. A wrong database connection string can bring down the entire platform; a missing environment variable can make features mysteriously disappear.

CoomiaPublished on July 5, 202515 min read
Share this articleTwitter / X

Configuration Management: YAML to Runtime Config Chain

Series: S2 Architecture Overview · Article 12 | Level: Intermediate | Reading Time: 18 min

#TL;DR

  • The coomia-dip platform uses a four-layer configuration model: code defaults, YAML files, environment variables, and runtime dynamic config — each layer overrides the previous, enabling seamless configuration transitions from development to production.
  • Three tech stacks (Spring Boot / Quarkus / FastAPI) each have their own config conventions; the platform unifies management through Docker Compose environment variable injection, with a single .env file controlling the entire cluster.
  • Sensitive configurations (database passwords, API keys) use Docker Secrets + environment variable separation, never hardcoded in YAML; Feature Flags use Ontology metadata-driven storage, supporting per-World granularity toggles.

#Introduction: Configuration Is the Nervous System of Distributed Systems

In a platform composed of three processes, configuration management appears simple but is actually the epicenter of operational pain. A wrong database connection string can bring down the entire platform; a missing environment variable can make features mysteriously disappear.

Code
Configuration Management Challenges:

1. Multi-Tech-Stack Unification
   onto-control:      Spring Boot (application.yml)
   onto-data:         Quarkus (application.properties)
   onto-intelligence: FastAPI (Python .env / pydantic-settings)

2. Multi-Environment Adaptation
   dev     -> Local development (SQLite / in-memory)
   test    -> CI/CD testing (Testcontainers)
   staging -> Pre-production (real infrastructure, small scale)
   prod    -> Production (HA, multi-replica)

3. Security Requirements
   Database passwords, Kafka auth, gRPC TLS certificates
   -> Must not appear in code repository
   -> Must not appear in Docker images

4. Dynamic Updates
   Rate limits, degradation switches, Feature Flags
   -> Must be modifiable without restart

This article provides a complete walkthrough of the coomia-dip configuration architecture — from static YAML to Docker Compose environment variables, from Secret management to runtime dynamic configuration.

#1. Four-Layer Configuration Model

#1.1 Configuration Priority

The platform follows the "convention over configuration" principle with four configuration layers, from lowest to highest priority:

Code
Configuration Priority (lowest to highest):

Layer 1: Code Defaults
  Location: Constants / default parameters in source code
  Purpose: Ensure system starts with zero config (development mode)
  Example: server.port = 8080

Layer 2: YAML / Properties Files (Static Config)
  Location: src/main/resources/application.yml
  Purpose: Base configuration for each environment
  Example: spring.datasource.url = jdbc:postgresql://...

Layer 3: Environment Variables
  Location: Docker Compose .env file / K8s ConfigMap
  Purpose: Override environment-specific configuration
  Example: SPRING_DATASOURCE_URL=jdbc:postgresql://prod-db:5432/onto

Layer 4: Runtime Dynamic Config
  Location: Database / config center
  Purpose: Dynamic adjustments without restart
  Example: rate_limit.max_requests_per_second = 500

Override order: Layer 4 > Layer 3 > Layer 2 > Layer 1

#1.2 When to Use Each Layer

Code
Configuration Classification Decision Tree:

This config value...
  |
  +-- Rarely changes? (ports, protocol versions)
  |   +-- -> Layer 1: Code defaults
  |
  +-- Differs by environment? (DB address, Kafka address)
  |   +-- -> Layer 2 + Layer 3: YAML + env var override
  |
  +-- Is sensitive? (passwords, keys)
  |   +-- -> Layer 3: Environment variables (injected from Secrets)
  |
  +-- Needs dynamic adjustment? (rate limits, toggles)
      +-- -> Layer 4: Runtime config

#2. Configuration Implementation by Process

#2.1 onto-control: Spring Boot Configuration

Spring Boot has the most powerful configuration capabilities among the three frameworks, supporting Profiles, YAML multi-documents, and property binding:

YAML
# onto-control/src/main/resources/application.yml
# -- Default config (all environments) --

server:
  port: 8080

spring:
  application:
    name: onto-control
  datasource:
    url: jdbc:postgresql://localhost:5432/onto_control
    username: onto
    password: onto_dev
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 10000
  kafka:
    bootstrap-servers: localhost:9092
    consumer:
      group-id: onto-control
      auto-offset-reset: earliest
    producer:
      acks: all

grpc:
  server:
    port: 9090
  client:
    onto-data:
      address: static://localhost:9091
      negotiation-type: plaintext
    onto-intelligence:
      address: static://localhost:9092
      negotiation-type: plaintext

# Resilience4j config
resilience4j:
  circuitbreaker:
    instances:
      data-service:
        sliding-window-size: 20
        failure-rate-threshold: 50
        wait-duration-in-open-state: 30s
      intelligence-service:
        sliding-window-size: 10
        failure-rate-threshold: 50
        wait-duration-in-open-state: 30s

---
# -- Production config --
spring:
  config:
    activate:
      on-profile: prod
  datasource:
    hikari:
      maximum-pool-size: 50
      minimum-idle: 10

grpc:
  server:
    security:
      enabled: true
      certificate-chain: file:/etc/certs/server.crt
      private-key: file:/etc/certs/server.key

#2.2 onto-data: Quarkus Configuration

Quarkus uses MicroProfile Config, supporting both properties and YAML formats:

PROPERTIES
# onto-data/src/main/resources/application.properties
# -- Default config --

quarkus.application.name=onto-data
quarkus.http.port=8081
quarkus.grpc.server.port=9091

# Doris datasource
quarkus.datasource.db-kind=mysql
quarkus.datasource.jdbc.url=jdbc:mysql://localhost:9030/onto_data
quarkus.datasource.username=root
quarkus.datasource.password=
quarkus.datasource.jdbc.max-size=30
quarkus.datasource.jdbc.min-size=5

# Nessie config
nessie.server.uri=http://localhost:19120/api/v2
nessie.default-branch=main

# Iceberg config
iceberg.catalog.type=nessie
iceberg.catalog.uri=http://localhost:19120/api/v2
iceberg.catalog.warehouse=s3://onto-warehouse/

# Kafka
kafka.bootstrap.servers=localhost:9092
mp.messaging.incoming.ontology-events.connector=smallrye-kafka
mp.messaging.incoming.ontology-events.topic=platform.ontology.events
mp.messaging.incoming.ontology-events.group.id=onto-data

# -- Production config (via env var override or profile) --
%prod.quarkus.datasource.jdbc.url=jdbc:mysql://doris-prod:9030/onto_data
%prod.quarkus.datasource.jdbc.max-size=100
%prod.quarkus.datasource.jdbc.min-size=20

#2.3 onto-intelligence: FastAPI + Pydantic Settings

The Python ecosystem uses pydantic-settings for type-safe configuration:

Python
# onto-intelligence/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import Field
from typing import Optional


class DatabaseSettings(BaseSettings):
    """Database configuration"""
    model_config = SettingsConfigDict(env_prefix="DB_")

    host: str = "localhost"
    port: int = 5432
    name: str = "onto_intelligence"
    user: str = "onto"
    password: str = "onto_dev"
    pool_size: int = 10
    pool_max_overflow: int = 20

    @property
    def url(self) -> str:
        return (
            f"postgresql+asyncpg://{self.user}:{self.password}"
            f"@{self.host}:{self.port}/{self.name}"
        )


class GrpcSettings(BaseSettings):
    """gRPC configuration"""
    model_config = SettingsConfigDict(env_prefix="GRPC_")

    server_port: int = 9092
    control_address: str = "localhost:9090"
    data_address: str = "localhost:9091"
    max_message_size: int = 50 * 1024 * 1024  # 50MB
    keepalive_time_ms: int = 30000
    keepalive_timeout_ms: int = 10000


class KafkaSettings(BaseSettings):
    """Kafka configuration"""
    model_config = SettingsConfigDict(env_prefix="KAFKA_")

    bootstrap_servers: str = "localhost:9092"
    group_id: str = "onto-intelligence"
    auto_offset_reset: str = "earliest"
    enable_auto_commit: bool = False


class TemporalSettings(BaseSettings):
    """Temporal configuration"""
    model_config = SettingsConfigDict(env_prefix="TEMPORAL_")

    server_address: str = "localhost:7233"
    namespace: str = "onto-intelligence"
    task_queue: str = "reasoning-tasks"
    max_concurrent_activities: int = 50
    max_concurrent_workflows: int = 100


class AppSettings(BaseSettings):
    """Top-level application configuration"""
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        env_nested_delimiter="__",
    )

    environment: str = "dev"
    debug: bool = False
    log_level: str = "INFO"

    db: DatabaseSettings = Field(default_factory=DatabaseSettings)
    grpc: GrpcSettings = Field(default_factory=GrpcSettings)
    kafka: KafkaSettings = Field(default_factory=KafkaSettings)
    temporal: TemporalSettings = Field(default_factory=TemporalSettings)


# Global settings singleton
settings = AppSettings()

#3. Docker Compose Unified Configuration Management

#3.1 Environment Variable File

All environment variables are centralized in a .env file, automatically loaded by Docker Compose:

Bash
# deployment-Layer/.env
# ===================================================
# coomia-dip Platform Unified Environment Configuration
# ===================================================

# -- Common --
PLATFORM_ENV=dev
PLATFORM_LOG_LEVEL=INFO

# -- PostgreSQL --
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_DB=onto_control
POSTGRES_USER=onto
POSTGRES_PASSWORD=change_me_in_production

# -- Doris --
DORIS_HOST=doris-fe
DORIS_PORT=9030
DORIS_USER=root
DORIS_PASSWORD=

# -- Kafka --
KAFKA_BOOTSTRAP_SERVERS=kafka:9092
KAFKA_NUM_PARTITIONS=6

# -- Nessie --
NESSIE_URI=http://nessie:19120/api/v2

# -- MinIO (S3-compatible storage) --
MINIO_ENDPOINT=http://minio:9000
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin
MINIO_BUCKET=onto-warehouse

# -- Temporal --
TEMPORAL_ADDRESS=temporal:7233
TEMPORAL_NAMESPACE=onto-intelligence

# -- gRPC Ports --
GRPC_CONTROL_PORT=9090
GRPC_DATA_PORT=9091
GRPC_INTELLIGENCE_PORT=9092

# -- onto-control specific --
SPRING_PROFILES_ACTIVE=docker
SPRING_DATASOURCE_URL=jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
SPRING_DATASOURCE_USERNAME=${POSTGRES_USER}
SPRING_DATASOURCE_PASSWORD=${POSTGRES_PASSWORD}

# -- onto-data specific --
QUARKUS_PROFILE=docker
QUARKUS_DATASOURCE_JDBC_URL=jdbc:mysql://${DORIS_HOST}:${DORIS_PORT}/onto_data

# -- onto-intelligence specific --
DB_HOST=${POSTGRES_HOST}
DB_PORT=${POSTGRES_PORT}
DB_NAME=onto_intelligence
DB_USER=${POSTGRES_USER}
DB_PASSWORD=${POSTGRES_PASSWORD}
GRPC_CONTROL_ADDRESS=onto-control:${GRPC_CONTROL_PORT}
GRPC_DATA_ADDRESS=onto-data:${GRPC_DATA_PORT}

#3.2 Docker Compose Configuration Injection

YAML
# deployment-Layer/docker-compose.yml (config-related sections)

services:
  onto-control:
    image: coomia-dip/control:latest
    environment:
      - SPRING_PROFILES_ACTIVE=${SPRING_PROFILES_ACTIVE}
      - SPRING_DATASOURCE_URL=${SPRING_DATASOURCE_URL}
      - SPRING_DATASOURCE_USERNAME=${SPRING_DATASOURCE_USERNAME}
      - SPRING_DATASOURCE_PASSWORD=${SPRING_DATASOURCE_PASSWORD}
      - SPRING_KAFKA_BOOTSTRAP_SERVERS=${KAFKA_BOOTSTRAP_SERVERS}
      - GRPC_SERVER_PORT=${GRPC_CONTROL_PORT}
      - GRPC_CLIENT_ONTO_DATA_ADDRESS=static://onto-data:${GRPC_DATA_PORT}
      - GRPC_CLIENT_ONTO_INTELLIGENCE_ADDRESS=static://onto-intelligence:${GRPC_INTELLIGENCE_PORT}
      - PLATFORM_LOG_LEVEL=${PLATFORM_LOG_LEVEL}
    ports:
      - "8080:8080"
      - "${GRPC_CONTROL_PORT}:${GRPC_CONTROL_PORT}"
    depends_on:
      postgres:
        condition: service_healthy
      kafka:
        condition: service_healthy

  onto-data:
    image: coomia-dip/data:latest
    environment:
      - QUARKUS_PROFILE=${QUARKUS_PROFILE}
      - QUARKUS_DATASOURCE_JDBC_URL=${QUARKUS_DATASOURCE_JDBC_URL}
      - QUARKUS_DATASOURCE_USERNAME=${DORIS_USER}
      - QUARKUS_DATASOURCE_PASSWORD=${DORIS_PASSWORD}
      - NESSIE_URI=${NESSIE_URI}
      - KAFKA_BOOTSTRAP_SERVERS=${KAFKA_BOOTSTRAP_SERVERS}
      - GRPC_SERVER_PORT=${GRPC_DATA_PORT}
      - MINIO_ENDPOINT=${MINIO_ENDPOINT}
      - MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY}
      - MINIO_SECRET_KEY=${MINIO_SECRET_KEY}
    ports:
      - "8081:8081"
      - "${GRPC_DATA_PORT}:${GRPC_DATA_PORT}"

  onto-intelligence:
    image: coomia-dip/intelligence:latest
    environment:
      - ENVIRONMENT=${PLATFORM_ENV}
      - LOG_LEVEL=${PLATFORM_LOG_LEVEL}
      - DB_HOST=${POSTGRES_HOST}
      - DB_PORT=${POSTGRES_PORT}
      - DB_NAME=${DB_NAME}
      - DB_USER=${DB_USER}
      - DB_PASSWORD=${DB_PASSWORD}
      - GRPC_SERVER_PORT=${GRPC_INTELLIGENCE_PORT}
      - GRPC_CONTROL_ADDRESS=${GRPC_CONTROL_ADDRESS}
      - GRPC_DATA_ADDRESS=${GRPC_DATA_ADDRESS}
      - KAFKA_BOOTSTRAP_SERVERS=${KAFKA_BOOTSTRAP_SERVERS}
      - TEMPORAL_SERVER_ADDRESS=${TEMPORAL_ADDRESS}
      - TEMPORAL_NAMESPACE=${TEMPORAL_NAMESPACE}
    ports:
      - "8082:8082"
      - "${GRPC_INTELLIGENCE_PORT}:${GRPC_INTELLIGENCE_PORT}"

#3.3 Multi-Environment Overrides

Code
Multi-Environment Configuration Strategy:

deployment-Layer/
+-- .env                         # Default env vars (dev)
+-- .env.staging                 # Staging overrides
+-- .env.prod                    # Production overrides
+-- docker-compose.yml           # Base Compose file
+-- docker-compose.staging.yml   # Staging overrides
+-- docker-compose.prod.yml      # Production overrides

Start Commands:
  dev:     docker compose up
  staging: docker compose --env-file .env.staging -f docker-compose.yml -f docker-compose.staging.yml up
  prod:    docker compose --env-file .env.prod -f docker-compose.yml -f docker-compose.prod.yml up

Production Override Example (docker-compose.prod.yml):
  services:
    onto-control:
      deploy:
        replicas: 2
        resources:
          limits:
            cpus: '4'
            memory: 8G
      environment:
        - SPRING_DATASOURCE_HIKARI_MAXIMUM_POOL_SIZE=50
        - JAVA_OPTS=-Xmx6g -Xms4g

#4. Sensitive Configuration (Secrets) Management

#4.1 Secret Separation Principle

Code
Secret Management Rules:

NEVER:
  - Hardcode passwords in source code
  - Write passwords in YAML config files committed to Git
  - Write passwords in Dockerfiles
  - Write passwords in docker-compose.yml

CORRECT:
  - .env file uses weak passwords, only for local development
  - .env file is in .gitignore (not committed to Git)
  - Provide .env.example template (passwords use change_me placeholder)
  - Production uses Docker Secrets or K8s Secrets

#4.2 Docker Secrets

YAML
# docker-compose.prod.yml using Docker Secrets
version: '3.8'

secrets:
  postgres_password:
    file: /run/secrets/postgres_password
  doris_password:
    file: /run/secrets/doris_password
  kafka_sasl_password:
    file: /run/secrets/kafka_sasl_password

services:
  onto-control:
    secrets:
      - postgres_password
    environment:
      - SPRING_DATASOURCE_PASSWORD_FILE=/run/secrets/postgres_password

  onto-data:
    secrets:
      - doris_password
    environment:
      - QUARKUS_DATASOURCE_PASSWORD_FILE=/run/secrets/doris_password

  onto-intelligence:
    secrets:
      - postgres_password
    environment:
      - DB_PASSWORD_FILE=/run/secrets/postgres_password

#4.3 Python Secret File Reading

Python
# Secret file support in onto-intelligence/config.py
from pydantic_settings import BaseSettings
from pydantic import model_validator
from pathlib import Path
from typing import Optional


class DatabaseSettings(BaseSettings):
    host: str = "localhost"
    port: int = 5432
    name: str = "onto_intelligence"
    user: str = "onto"
    password: str = "onto_dev"
    password_file: Optional[str] = None  # Docker Secret file path

    @model_validator(mode="after")
    def read_password_from_file(self) -> "DatabaseSettings":
        """If password_file is provided, read password from file"""
        if self.password_file:
            secret_path = Path(self.password_file)
            if secret_path.exists():
                self.password = secret_path.read_text().strip()
        return self

#5. Feature Flag System

#5.1 Ontology-Driven Feature Flags

The platform's Feature Flags don't use third-party services (like LaunchDarkly) but leverage its own Ontology model for management:

Code
Feature Flag Storage Model:

ObjectType: FeatureFlag
Properties:
  +-- flag_key: String (unique identifier, e.g. "enable_derived_properties")
  +-- flag_type: Enum (BOOLEAN, STRING, NUMBER, JSON)
  +-- default_value: String (default value)
  +-- description: String (feature description)
  +-- enabled: Boolean (global toggle)
  +-- created_at: Timestamp
  +-- updated_at: Timestamp

ObjectType: FeatureFlagOverride
Properties:
  +-- flag_key: String (associated Flag)
  +-- scope_type: Enum (WORLD, USER, ROLE)
  +-- scope_value: String (World ID / User ID / Role name)
  +-- override_value: String (override value)
  +-- priority: Integer (higher = more priority)

#5.2 Feature Flag Evaluation Logic

Code
Feature Flag Evaluation Flow:

evaluate(flag_key, context) {
  1. Look up FeatureFlag object
     +-- Not found -> return null (feature undefined)
     +-- Found -> continue

  2. Check global toggle
     +-- enabled = false -> return default_value
     +-- enabled = true -> continue

  3. Find Overrides (by priority descending)
     +-- Match USER scope -> return override_value
     +-- Match ROLE scope -> return override_value
     +-- Match WORLD scope -> return override_value
     +-- No match -> return default_value
}

Example:
  flag_key: "enable_derived_properties"
  default_value: "false"
  overrides:
    - scope: WORLD, value: "world-staging", override: "true"    (staging World enabled)
    - scope: USER, value: "admin-001", override: "true"         (admin enabled)

  evaluate("enable_derived_properties", {world: "world-prod", user: "user-001"})
    -> "false" (default)

  evaluate("enable_derived_properties", {world: "world-staging", user: "user-001"})
    -> "true" (World override)

#5.3 Feature Flag Clients by Process

Java
// Feature Flag usage in onto-control
@Service
public class FeatureFlagService {

    private final FeatureFlagRepository flagRepo;
    private final LoadingCache<String, FeatureFlag> cache;

    public FeatureFlagService(FeatureFlagRepository flagRepo) {
        this.flagRepo = flagRepo;
        this.cache = Caffeine.newBuilder()
            .maximumSize(200)
            .expireAfterWrite(Duration.ofMinutes(5))
            .build(flagRepo::findByKey);
    }

    public boolean isEnabled(String flagKey, EvaluationContext context) {
        var flag = cache.get(flagKey);
        if (flag == null) return false;
        return flag.evaluate(context);
    }
}

// Usage example
public class ActionServiceImpl {
    public ActionResult executeAction(ActionRequest request) {
        if (!featureFlags.isEnabled("enable_action_v2", context)) {
            return executeActionV1(request);  // old logic
        }
        return executeActionV2(request);  // new logic
    }
}
Python
# Feature Flag usage in onto-intelligence
class FeatureFlagClient:
    """Feature Flag client - fetches flag state from onto-control"""

    def __init__(self, control_stub: ControlServiceStub):
        self.stub = control_stub
        self._cache: dict[str, CachedFlag] = {}
        self._cache_ttl = timedelta(minutes=5)

    async def is_enabled(
        self, flag_key: str, context: EvaluationContext | None = None
    ) -> bool:
        cached = self._cache.get(flag_key)
        if cached and not cached.is_expired:
            return cached.evaluate(context)

        # Fetch latest value from onto-control
        response = await self.stub.GetFeatureFlag(
            GetFeatureFlagRequest(key=flag_key)
        )
        flag = CachedFlag.from_response(response, self._cache_ttl)
        self._cache[flag_key] = flag
        return flag.evaluate(context)

#6. Configuration Hot-Reloading

#6.1 Which Configs Support Hot-Reload

Code
Configuration Hot-Reload Capability Matrix:

Config Type               Hot-Reload?   Update Method
──────────────────────────────────────────────────────
Feature Flags             Yes           Ontology change -> event notification
Log level                 Yes           Admin API / Actuator
Rate limit thresholds     Yes           Resilience4j runtime modification
Circuit breaker params    Yes           Resilience4j runtime modification
gRPC timeouts             Partial       Requires Channel rebuild
DB connection pool size   Partial       HikariCP supports runtime adjustment
Server ports              No            Requires restart
gRPC TLS certificates     No            Requires restart
Kafka Consumer Group      No            Requires restart

#6.2 Dynamic Log Level Adjustment

Code
onto-control (Spring Boot Actuator):
  POST /actuator/loggers/com.onto.control
  Body: {"configuredLevel": "DEBUG"}
  -> Takes effect immediately, no restart needed

onto-data (Quarkus):
  # Quarkus doesn't natively support runtime log level changes
  # Requires config change + restart
  # But can be implemented via custom endpoint

onto-intelligence (Python logging):
  POST /admin/log-level
  Body: {"logger": "onto_intelligence", "level": "DEBUG"}
  -> Calls logging.getLogger(name).setLevel(level)
  -> Takes effect immediately

#6.3 Configuration Change Events

Configuration changes are broadcast to all processes via Kafka events:

Code
Config Change Event Flow:

Admin modifies Feature Flag
    |
    +-- onto-control updates PostgreSQL
    |
    +-- Publishes Kafka event
    |   topic: platform.config.events
    |   {
    |     "type": "FEATURE_FLAG_CHANGED",
    |     "key": "enable_derived_properties",
    |     "old_value": "false",
    |     "new_value": "true",
    |     "scope": "WORLD:world-staging",
    |     "changed_by": "admin-001",
    |     "timestamp": "2026-03-24T14:30:00Z"
    |   }
    |
    +-- onto-data receives event
    |   -> Clears local Feature Flag cache
    |   -> Next request fetches fresh value
    |
    +-- onto-intelligence receives event
        -> Clears local Feature Flag cache
        -> Next request fetches fresh value

#7. Configuration Validation and Startup Checks

#7.1 Startup Configuration Validation

Each process executes configuration validation at startup to ensure required configs exist and are valid:

Python
# Startup config validation in onto-intelligence
class ConfigValidator:
    """Config validator - runs at startup"""

    def validate(self, settings: AppSettings) -> list[str]:
        errors: list[str] = []

        # Required field checks
        if not settings.db.host:
            errors.append("DB_HOST is required")
        if not settings.grpc.control_address:
            errors.append("GRPC_CONTROL_ADDRESS is required")
        if not settings.kafka.bootstrap_servers:
            errors.append("KAFKA_BOOTSTRAP_SERVERS is required")

        # Connectivity checks
        if not self._check_db_connection(settings.db):
            errors.append(
                f"Cannot connect to database at {settings.db.host}:{settings.db.port}"
            )
        if not self._check_grpc_connection(settings.grpc.control_address):
            errors.append(
                f"Cannot connect to onto-control at {settings.grpc.control_address}"
            )

        # Value range checks
        if settings.db.pool_size < 1 or settings.db.pool_size > 100:
            errors.append(f"DB_POOL_SIZE must be 1-100, got {settings.db.pool_size}")
        if settings.grpc.max_message_size > 100 * 1024 * 1024:
            errors.append("GRPC_MAX_MESSAGE_SIZE exceeds 100MB limit")

        return errors

#7.2 Configuration Diagnostic Endpoints

Each process exposes diagnostic endpoints for operational troubleshooting:

Code
Config Diagnostic APIs:

onto-control:
  GET /actuator/env          -- View all env vars (sensitive values masked)
  GET /actuator/configprops  -- View all config properties and their sources
  GET /actuator/info         -- View app info (version, build time)

onto-data:
  GET /q/config              -- View Quarkus configuration

onto-intelligence:
  GET /admin/config          -- View current effective config (passwords masked)
  GET /admin/config/sources  -- View each config item's source (code default/env/file)

#7.3 Configuration Drift Detection

Code
Configuration Drift Detection (runs hourly):

1. Collect current effective config from each process
2. Compare against expected config (.env file)
3. Detect unexpected differences

Detection Items:
  +-- Whether env vars were overridden by runtime changes
  +-- Whether Feature Flags match expectations
  +-- Whether connection pool params match
  +-- Whether log levels were temporarily changed

Drift Report:
  +----------------------------------------------------+
  | Configuration Drift Report - 2026-03-24 15:00      |
  +----------------------------------------------------+
  | onto-control:                                      |
  |   WARNING: log.level: expected=INFO, actual=DEBUG  |
  |      (manually changed via Actuator at 14:30)      |
  |   OK: datasource.pool-size: expected=20, actual=20 |
  |                                                    |
  | onto-intelligence:                                 |
  |   OK: All configs match expected values            |
  |                                                    |
  | Feature Flags:                                     |
  |   WARNING: enable_derived_properties: expected=false|
  |      actual=true (override for WORLD:world-staging)|
  +----------------------------------------------------+

#8. Configuration Best Practices

#8.1 Naming Conventions

Code
Configuration Naming Conventions:

Environment Variable Names:
  Format: {NAMESPACE}_{COMPONENT}_{KEY}
  Examples:
    SPRING_DATASOURCE_URL             (Spring Boot standard)
    QUARKUS_DATASOURCE_JDBC_URL       (Quarkus standard)
    DB_HOST                           (Python custom)
    GRPC_CONTROL_ADDRESS              (cross-language)
    KAFKA_BOOTSTRAP_SERVERS           (cross-language)

YAML Key Names:
  Format: kebab-case (Spring Boot) / dot-separated (Quarkus)
  Examples:
    spring.datasource.hikari.maximum-pool-size
    quarkus.datasource.jdbc.max-size

Python Config Names:
  Format: snake_case (Pydantic fields)
  Examples:
    db.pool_size
    grpc.control_address

#8.2 Environment Variable Override Rules

Code
Environment Variable to Config Mapping Rules:

Spring Boot:
  SPRING_DATASOURCE_URL -> spring.datasource.url
  Rule: _ replaced with . , all lowercase

Quarkus:
  QUARKUS_DATASOURCE_JDBC_URL -> quarkus.datasource.jdbc.url
  Rule: Same as Spring Boot

Pydantic Settings:
  DB_HOST -> DatabaseSettings.host (via env_prefix="DB_")
  GRPC__CONTROL_ADDRESS -> GrpcSettings.control_address (via env_nested_delimiter="__")

#8.3 New Configuration Checklist

Code
Checklist for Adding a New Configuration Item:

[ ] Define reasonable default value in code (Layer 1)
[ ] Add commented example in YAML/properties (Layer 2)
[ ] Add environment variable to .env.example (Layer 3)
[ ] If sensitive, ensure injection only via env vars/Secrets
[ ] Add config validation (startup check)
[ ] Update diagnostic endpoint masking rules
[ ] Document the new config item
[ ] Test: starts normally with defaults (no config)
[ ] Test: environment variable override works
[ ] Test: invalid values are caught by validation

#Key Takeaways

  1. Four-Layer Model: Code defaults, YAML, environment variables, runtime config — each overrides the previous, balancing development convenience and production security.
  2. Docker Compose Unification: A single .env file controls configuration for all three processes, eliminating scattered config files.
  3. Secret Separation: Sensitive configs are injected via Docker Secrets, never appearing in code repositories or images.
  4. Self-Bootstrapping Feature Flags: Leverages the Ontology model to store Flag definitions, supporting per-World/User/Role granularity control.
  5. Config Validation: Automatic startup validation of required fields, connectivity, and value ranges — fail fast rather than fail mysteriously at runtime.
  6. Hot-Reload Boundaries: Clear distinction between hot-reloadable and restart-required configs, preventing operational mistakes.
  7. Drift Detection: Periodic comparison of actual vs. expected configuration, alerting on unexpected differences.

#Next Article

The next article, S2-13 AI + Human Collaboration: 10x Efficiency with Claude, will share the coomia-dip development methodology — how AI programming assistants (Claude) write 70-80% of the code, deliver a complete gRPC Service in 2-4 hours, and how CLAUDE.md guardrails ensure the quality of AI-generated code.

tags: configuration, YAML, Docker-Compose, environment-variables, secrets, feature-flags, hot-reload, pydantic-settings, Spring-Boot, Quarkus