0013-Module configuration management

0013-Module configuration management

Start Date

Jun 1, 2026

End Date

 

Contributors

@Oleksii Petrenko @Mikita Siadykh @oleksandr_haimanov (Unlicensed) @Artem Akimov @Denis @Viktor Gema @Drif Abdnour

@Roman_Fedynyshyn

Status

PRELIMINARY REVIEW

Summary

FOLIO currently lacks a unified, standardized model for module configuration management. Configuration data is fragmented across environment variables, module descriptors, database entries, and Helm/Kubernetes manifests, with no single source of truth. This RFC proposes a governed configuration management approach that introduces a standardized CompleteConfiguration.json artifact per module, a structured review workflow, and clear ownership and classification rules for all configuration variables. The goal is to reduce operational risk, improve discoverability, and establish a repeatable process for configuration evolution across FOLIO's growing ecosystem of independently deployable modules.

Motivation

FOLIO is a modular library services platform composed of independently versioned backend and UI modules deployed across distributed infrastructure. As the ecosystem scales across institutions and hosting providers, the absence of a shared configuration standard is causing compounding problems:

  • Configuration is spread across multiple artifacts and locations with no single source of truth, making it impossible to quickly answer what configuration keys exist, which are required, or which are deprecated. Artifacts that might be replaces

  • Different modules use different naming conventions, default-handling strategies, and validation approaches, resulting in unpredictable operational behavior.

  • Configuration drift between Dev, SprintTesting, Bugfest, and Production environments causes 'works in test but not in prod' failures.

  • Module upgrades lack a systematic migration strategy for configuration evolution, increasing release risk.

  • Onboarding new institutions and hosting providers requires disproportionate manual effort from DevOps and support teams.

The expected outcome of this RFC is a governed, discoverable, and version-aware configuration management model that reduces operational overhead, lowers release risk, and scales reliably with the FOLIO community. It directly supports the strategic goal of making FOLIO easier to operate at scale.

Scope

In Scope

  • Definition and format of the standardized CompleteConfiguration.json artifact stored in new repository.

  • Classification taxonomy for configuration variables (type, sensitivity, scope, default value rules).

  • Workflow.

  • Ownership model: responsibilities of development and Kitfox teams.

Out of Scope

  • Implementation of a centralized configuration registry platform (identified as a future action item, pending requirements from this RFC).

  • Changes to ModuleDescriptor or DeploymentDescriptor formats.

  • UI-level configuration management interfaces.

  • Secrets management and credential vaulting solutions.

  • Configuration management for non-FOLIO infrastructure components.

Detailed Explanation/Design

The CompleteConfiguration.json Artifact

The core of this proposal is a standardized file — CompleteConfiguration.jsonplaced at a root level of each module and owned by the responsible development team. It serves as the single source of truth for all module-level configuration variables, replacing the current fragmented approach where configuration is scattered across environment variables, module descriptors, tenant settings, and deployment manifests.

Default values are calibrated against a defined reference baseline of bugfest environment (currently 3 standalone tenants, and 11 consortia tenants). The format is being validated through a PoC in mod-bulk-operations, with planned extensions for Spring-style environment profiles and automated schema validation at build time.

Example

{ "description": "mod-bulk-operations configuration template", "moduleDefaults": { "resources": [ { "name": "cpu", "value": "64", "description": "CPU request/limit for container" }, { "name": "memory", "value": "3072", "description": "Memory hard limit for container" }, { "name": "memoryReservation", "value": "2600", "description": "Memory reservation for container" } ], "java": [ { "name": "xmx", "value": "2048m", "description": "Maximum Java heap size" }, { "name": "MaxMetaspaceSize", "value": "512m", "description": "Maximum Java metaspace size" }, { "name": "MetaspaceSize", "value": "384m", "description": "Initial Java metaspace size" } ], "deployment": { "isHaSupported": true, "replicasRecommendedCount": 2, "description": "Deployment configuration for the backend module. Adjust replicas based on expected load and HA requirements. Set to false if only single instance is supported or required." }, "kong": { "timeouts": { "connect": "60s", "send": "60s", "read": "60s" }, "description": "Custom Kong proxy timeouts. Use this only if different from default Kong configuration" }, "EnvVars": [ { "name": "DB_DATABASE", "value": null, "description": "Database name", "type": "DB", "essential": true }, { "name": "DB_EXPLAIN_QUERY_THRESHOLD", "value": "300000", "description": "Threshold for explaining slow queries in milliseconds", "type": "DB", "essential": false }, { "name": "DB_HOST", "value": null, "description": "Database host", "type": "DB", "essential": true }, { "name": "DB_MAXPOOLSIZE", "value": "50", "description": "Maximum database connection pool size", "type": "DB", "essential": false }, { "name": "DB_PASSWORD", "value": null, "description": "Database password", "type": "DB", "essential": true }, { "name": "DB_PORT", "value": null, "description": "Database port", "type": "DB", "essential": true }, { "name": "DB_USERNAME", "value": null, "description": "Database username", "type": "DB", "essential": true }, { "name": "ENV", "value": null, "description": "Environment", "type": "CONFIG", "essential": false }, { "name": "JAVA_ARGS", "value": null, "description": "Java arguments", "type": "CONFIG", "essential": true }, { "name": "JAVA_OPTS", "value": null, "description": "Java options", "type": "CONFIG", "essential": true }, { "name": "LOG4J_CONFIGURATION_FILE", "value": null, "description": "Log4j configuration file", "type": "CONFIG", "essential": false }, { "name": "KAFKA_HOST", "value": null, "description": "Kafka host", "type": "KAFKA", "essential": true }, { "name": "KAFKA_PORT", "value": null, "description": "Kafka port", "type": "KAFKA", "essential": true }, { "name": "S3_ACCESS_KEY_ID", "value": null, "description": "S3 access key", "type": "S3", "essential": true }, { "name": "S3_BUCKET", "value": null, "description": "S3 bucket", "type": "S3", "essential": true }, { "name": "S3_IS_AWS", "value": true, "description": "Specify if AWS S3 is used as files storage", "type": "S3", "essential": true }, { "name": "S3_REGION", "value": null, "description": "S3 region", "type": "S3", "essential": true }, { "name": "S3_SECRET_ACCESS_KEY", "value": null, "description": "S3 secret key", "type": "S3", "essential": true }, { "name": "S3_SUB_PATH", "value": "mod-bulk-operations", "description": "S3 subpath for file storage", "type": "S3", "essential": false }, { "name": "S3_URL", "value": null, "description": "S3 url", "type": "S3", "essential": true }, { "name": "SYSTEM_USER_NAME", "value": "mod-bulk-operations", "description": "Username of the system user", "type": "FUNCTIONAL", "essential": true }, { "name": "SYSTEM_USER_PASSWORD", "value": "{SYSTEM_USER_PASSWORD}", "description": "Password of the system user", "type": "FUNCTIONAL", "essential": true }, { "name": "SYSTEM_USER_ENABLED", "value": true, "description": "Defines if system user must be created at service tenant initialization or used for egress service requests", "type": "FUNCTIONAL", "essential": true }, { "name": "PLATFORM", "value": "eureka", "description": "Specifies if okapi or eureka platform", "type": "FUNCTIONAL", "essential": true }, { "name": "MAX_UPLOADING_RETRIES", "value": "50", "description": "Max uploading retry", "type": "FUNCTIONAL", "essential": false }, { "name": "DI_NUMBER_OF_CONCURRENT_REQUESTS", "value": "5", "description": "DI_NUMBER_OF_CONCURRENT_REQUESTS", "type": "FUNCTIONAL", "essential": false }, { "name": "DI_CHUNK_SIZE", "value": "1000", "description": "DI_NUMBER_OF_CONCURRENT_REQUESTS", "type": "FUNCTIONAL", "essential": false }, { "name": "FQM_MAX_CHUNK_SIZE", "value": "10000", "description": "Max chunk size of FQM Fetcher", "type": "FUNCTIONAL", "essential": false }, { "name": "FQM_MAX_PARALLEL_CHUNKS", "value": "10", "description": "Max number of parallel chunks processed at the same time", "type": "FUNCTIONAL", "essential": false }, { "name": "FQM_QUERY_APPROACH", "value": true, "description": "Approach to use with query: false if identifiers flow, otherwise FQM flow for both Query and Identifiers approaches", "type": "FUNCTIONAL", "essential": true } ] }, "sidecarDefaults": { "resources": [ { "name": "cpu", "value": "64", "description": "CPU request/limit for container" }, { "name": "memory", "value": "1500", "description": "Memory hard limit for container" }, { "name": "memoryReservation", "value": "768", "description": "Memory reservation for container" } ], "java": [ { "name": "xms", "value": "128m", "description": "Initial Java heap size" }, { "name": "xmx", "value": "256m", "description": "Maximum Java heap size" }, { "name": "MaxMetaspaceSize", "value": "128m", "description": "Maximum Java metaspace size" } ], "EnvVars": [ { "name": "ENV", "value": null, "description": "Environment", "type": "CONFIG", "essential": false }, { "name": "JAVA_OPTS", "value": null, "description": "Java options", "type": "CONFIG", "essential": false }, { "name": "KC_AUTHORIZATION_CACHE_MAX_SIZE", "value": "5000", "description": "Maximum amount of entries for keycloak authorization cache.", "type": "CACHE", "essential": false }, { "name": "KC_URI_VALIDATION_ENABLED", "value": false, "description": "Defines if JsonWebTokenParser compares URI from token with KC_URL value.", "type": "KC", "essential": false }, { "name": "QUARKUS_HTTP_LIMITS_MAX_BODY_SIZE", "value": "200M", "description": "", "type": "QUARKUS", "essential": false }, { "name": "QUARKUS_HTTP_LIMITS_MAX_INITIAL_LINE_LENGTH", "value": "8192", "description": "", "type": "QUARKUS", "essential": false }, { "name": "QUARKUS_OTEL_SDK_DISABLED", "value": true, "description": "", "type": "QUARKUS", "essential": false }, { "name": "QUARKUS_REST_CLIENT_CONNECT_TIMEOUT", "value": "180000", "description": "", "type": "QUARKUS", "essential": false }, { "name": "QUARKUS_REST_CLIENT_READ_TIMEOUT", "value": "180000", "description": "", "type": "QUARKUS", "essential": false }, { "name": "QUARKUS_REST_CLIENT_SEND_TIMEOUT", "value": "180000", "description": "", "type": "QUARKUS", "essential": false }, { "name": "QUARKUS_THREAD_POOL_MAX_THREADS", "value": "4", "description": "", "type": "QUARKUS", "essential": false }, { "name": "QUARKUS_VERTX_EVENT_LOOPS_POOL_SIZE", "value": "1", "description": "", "type": "QUARKUS", "essential": false }, { "name": "REQUEST_TIMEOUT", "value": "604800000", "description": "Configures the amount of time in milliseconds after which if the request does not return any data within the timeout period fails the request.", "type": "CONFIG", "essential": false }, { "name": "SIDECAR_FORWARD_UNKNOWN_REQUESTS", "value": true, "description": "Set behavior for EGRESS request. If there are no information about discovery, request will be forwarded to the SIDECAR_FORWARD_UNKNOWN_REQUESTS_DESTINATION. If SIDECAR_FORWARD_UNKNOWN_REQUESTS is enabled, ROUTING_DYNAMIC_ENABLED should be disabled.", "type": "CONFIG", "essential": false }, { "name": "TOKEN_CACHE_MAX_CAPACITY", "value": "5000", "description": "Token cache max capacity.", "type": "CACHE", "essential": false }, { "name": "TOKEN_CACHE_REFRESH_PRIOR_EXPIRATION", "value": "595", "description": "Specifies the amount of seconds for a cache entry invalidation prior to the token expiration.", "type": "CACHE", "essential": false } ] } }

Why a New Artifact, Not an Extension of Existing Ones

The working group evaluated extending both ModuleDescriptor and DeploymentDescriptor. ModuleDescriptor was ruled out because it captures interface dependencies and permissions — mixing in deployment-specific configuration would conflate two distinct concerns. DeploymentDescriptor is fully outdated and scheduled for decommissioning. A purpose-built file avoids those constraints and can be designed purely for the operational audience: SysOps teams deploying and tuning FOLIO modules.

Workflow

The goal of workflow to ensure that teams keep configuration up-to-date and environments (Rancher - Kitfox and Bugfest - FSE) are updated, for this purpose teams need to create tickets (Kitfox should update folio-helm-2 so it can be reused by other hosting providers).

Risks and Drawbacks

  • Adoption overhead: requiring all module teams to create and maintain CompleteConfiguration.json adds ongoing maintenance work. Teams with limited capacity may fall behind, reducing the value of the approach if adoption is uneven.

  • Staleness risk: if the configuration file is not kept in sync with actual module behavior, it becomes misleading rather than helpful. Enforcement mechanisms (CI validation, release checklists) are necessary but not yet fully defined.

  • PTF review bottleneck: routing configuration PRs through mandatory PTF review when overriding PTF-defined values introduces a potential bottleneck, particularly during high-volume release periods.

  • Scope creep: the desired future state includes a centralized configuration registry, versioned migration support, and environment promotion tooling. These are valuable but complex, and deferring them (as this RFC proposes) means teams will work with the file-based approach for an indeterminate period before the full vision is realized.

  • Sidecar and generic parameters: the template for sidecar environment variables and generic parameters (Java memory, Kong timeouts) is still under development, meaning some coverage gaps will exist at initial rollout.

Rationale and Alternatives

Why This Design

A file-based artifact is the fastest path to a single source of truth without requiring new infrastructure. Storing CompleteConfiguration.json files in related module keeps configuration versioned alongside code, and makes it accessible to all teams without additional tooling.

The two-phase workflow mirrors FOLIO's existing operational rhythm (development, release) rather than introducing a parallel process, minimizing change management friction.

Alternatives Considered

  • Extending ModuleDescriptor: rejected because it conflates deployment concerns with module-internal interface and permission declarations.

  • Reusing DeploymentDescriptor: rejected because it is fully outdated and scheduled for decommissioning.

  • Centralized configuration registry (platform): desirable as a long-term goal, but requires substantial infrastructure work and specification of requirements first. This RFC establishes the requirements that would inform such a platform.

  • No standardization: the status quo results in ongoing operational fragility, increased cognitive load, and compounding risk as FOLIO scales. Not acting is not a viable option.

Prior Art

Spring Boot's application.properties and application.yml patterns, combined with profile-based configuration, served as inspiration for the profiling concept. The idea of a per-module configuration descriptor is also analogous to how Helm chart values.yaml files document configuration for Kubernetes deployments, a pattern familiar to the FOLIO DevOps community.

Timing

Target compliance for new modules: all modules introduced after this RFC is accepted must include a CompleteConfiguration.json at their first release.

Target compliance for existing modules: existing modules should add CompleteConfiguration.json no later than the Trillium (R1 2026) CSP#1 release cycle, prioritizing modules with the highest operational impact.

Unresolved Questions

  • Configuration management platform: what are the full requirements for a centralized registry? This RFC's outputs (file format, classification taxonomy, workflow) are intended to inform that specification.

  • Profiling mechanism: how should multiple configuration profiles (small, medium, large) be structured within CompleteConfiguration.json? The Spring-profile analogy is directional but the exact format is TBD.

  • Build-time validation: what schema validation tooling should be used, and at which exact CI stage should validation occur?

  • Who recommends configuration: for new modules or new variables with no PTF history, who provides the initial recommended values? This governance gap is open.

  • FSE configuration for various setups: a full FSE Configuration Management guide for various deployment setups (standalone, consortia, hosted) is in backlog.

  • How to track configuration changes to DevEnvs: the current guidance is that DevOps should use the provided configuration as a reference and adjust based on actual environment consumption, but no tooling supports this tracking yet.

  • Sidecar env vars template: the generic template for sidecar-specific environment variables is in progress but not yet finalized.

Keywords

configuration management, module configuration, environment variables, CompleteConfiguration.json, configuration review, PTF, FSE, Kitfox, release process, operational standards, FOLIO modules, feature flags, cache parameters, configuration drift, multi-tenant

Comments