0007 - A Typed Settings System Instead of Scattered config.getString(...) Calls¶
Status¶
Accepted
Context¶
config.yml was always meant to carry more than the handful of values
YamlConfigService originally read directly from FileConfiguration
(database, locale). Adding GUI, audit, module-toggle, performance, and
maintenance settings on top grows the number of config values considerably
- and with it the risk that every spot in the code brings its own
config.getString("something") call with its own default and its own (or
missing) validation. That's exactly the pattern the
development rules explicitly
mean to avoid.
At the same time, the system should later be usable by modules and extensions too (see decisions/0005-extension-ready-design.md) - not just the core.
Decision¶
- One access path from
config.ymlto application code: a registeredSettingDefinition<T>(key, type, default, description,requiresRestartflag, validator), resolved viaSettingsService.get(key). No code outsidedev.universaladmin.settings.YamlSettingsServicereadsconfig.ymldirectly. SettingKey<T>deliberately separates two strings: theconfigPath(the literal YAML path, e.g.gui.page-size- globally unique across every namespace, because it's the same line in the same file) and thenamespace(who owns the setting -core, a module viaModuleDescriptor.settingsNamespace(), later an extension id). See ../modules.md for the connection toModuleDescriptor.- Parsing (
SettingType<T>) and validation (SettingValidator<T>) are separate, combinable building blocks rather than one monolithic parse function per setting. - An invalid value never crashes the server.
YamlSettingsServicefalls back to the registered default on a parse or validation error and logs a clear warning - both at the initial start and on/admin reload. config-versionplusConfigMigrationRunner(in the now-leanerconfigpackage) versions the file itself, analogous tostorage.Migrationfor the database schema - an existing user's config is never silently overwritten on an update.
Detail: docs/development/settings.md.
Consequences¶
- New config values need more ceremony than a one-line
getString(...)call (aSettingKeyconstant, aSettingDefinitionregistration). Accepted as the price for central type safety, validation, and a uniform reload mechanism. - Every setting explicitly declares whether it's live-changeable
(
requiresRestart). That forces a deliberate decision per value instead of a blanket "reload just refreshes everything" - seeReloadConfigAction. - The
configandsettingspackages are now clearly separated:configonly knows about file versioning,settingsowns the actual type system. That's a visible shift from the access originally centralized inconfig.ConfigService.
Alternatives¶
- A generic
Map<String, Object>-based config object without registration: saves the definition ceremony, but loses the central place where every setting (with description, default, restart flag) is visible for docs/GUI/extensions - exactly whatSettingRegistryprovides. - Adopt an existing config framework (e.g. Configurate): would have
brought similar type safety, but an additional dependency for a problem
solvable with the already-present Bukkit
FileConfigurationand a thin layer of our own - see "no new dependencies without a clear reason" in the development rules.