Files
netneutrality/.junie/plans/remove-settings-singleton.md
T
anthorob 64f4890931 Refactor settings architecture and remove singleton pattern
Reworked `Settings` class to replace the static singleton pattern with constructor injection of `SettingsProvider`, improving testability and modularity. Updated all affected classes, tests, and configurations to align with the new architecture.
2026-09-10 05:59:55 -04:00

4.7 KiB

sessionId
sessionId
session-260909-154047-1h6n

Requirements

Overview & Goals

Remove the singleton pattern from Settings so it becomes a plain instantiable class. All access should go through constructor injection of SettingsProvider, except for the single bootstrap site in App.main() which creates the instance.

Scope

In Scope:

  • Remove static INSTANCE field and getInstance() from Settings
  • Remove static helper methods getDiscordInvite() and getVersion() from Settings
  • Migrate all remaining Settings.getInstance() call sites to use injected SettingsProvider
  • Update the test to construct Settings directly

Out of Scope:

  • Changing the SettingsProvider interface
  • Introducing a DI framework
  • Changing how properties are loaded

Technical Design

Current Implementation

  • Settings is a lazy singleton (getInstance()) implementing SettingsProvider
  • SettingsProvider interface already exists and is injected into most classes via ApplicationContext
  • App.buildContext() already registers the Settings instance as SettingsProvider in the context
  • Remaining direct singleton usages (9 call sites):
    • App.java — VERSION static field (line 51), main() bootstrap (lines 61-62), startup() (line 89), buildContext() (line 112), initJDA() (line 190)
    • CommandContext.java — isMessageFromCreator() (line 134)
    • WebModule.java — createRequest() (line 289) via Settings.getVersion()
    • ReportBug.java — executeTask() (line 34) via Settings.getDiscordInvite()
    • SettingsTest.java — getMavenVersion() test (line 17)

Proposed Changes

1. Settings.java

  • Make constructor public
  • Remove static INSTANCE field, getInstance(), getDiscordInvite(), getVersion()
  • Keep getMavenVersion() as an instance method
  • Add getDiscordInvite() as an instance method on SettingsProvider (or keep it as a convenience on Settings)

2. App.java

  • Create Settings via new Settings() at the top of main(); pass it into App constructor or store as a field
  • Replace VERSION static field with an instance field initialized from the settings instance
  • Thread the SettingsProvider through startup(), buildContext(), initJDA()

3. CommandContext.java

  • Add a SettingsProvider field, accept it via constructor parameter
  • Update isMessageFromCreator() to use the injected provider
  • Update subclasses (MessageCommandContext, SlashCommandContext, etc.) to pass it through

4. WebModule.java

  • Accept a version string or SettingsProvider in the Builder; replace Settings.getVersion() call

5. ReportBug.java

  • Accept SettingsProvider via constructor injection; replace Settings.getDiscordInvite() with settings.get("NETNEUTRALITY_BOT_INVITE", ...)

6. SettingsTest.java

  • Use new Settings() instead of Settings.getInstance()

Risks

  • CommandContext is abstract with multiple subclasses — all constructors need updating
  • WebModule.Builder is used in many places including inside Settings.getGitVersion() itself — the version string approach (pass a String rather than the full provider) keeps this simpler

Delivery Steps

✓ Step 1: Remove singleton from Settings class

Settings can be instantiated with new Settings() and no longer holds static state.

  • Make the Settings constructor public
  • Remove the static INSTANCE field and getInstance() method
  • Remove static helpers getDiscordInvite() and getVersion()
  • Move getDiscordInvite() logic to an instance method (or inline at call sites)
  • Keep getMavenVersion() as-is (already instance method)

✓ Step 2: Migrate App.java bootstrap to explicit instantiation

App creates and threads a single Settings instance through its lifecycle methods.

  • Create new Settings() at the start of main() and store as a field on App
  • Replace Settings.getInstance() calls in main(), startup(), buildContext(), initJDA() with the field
  • Replace the VERSION static field with an instance field or a local derived from the settings instance

✓ Step 3: Inject SettingsProvider into remaining call sites

CommandContext, WebModule, and ReportBug no longer reach for the singleton.

  • Add SettingsProvider constructor parameter to CommandContext; update isMessageFromCreator() to use it
  • Update CommandContext subclasses (MessageCommandContext, SlashCommandContext, etc.) to pass the provider through
  • In WebModule.Builder, accept a version string parameter; replace Settings.getVersion() in createRequest()
  • In ReportBug, add SettingsProvider constructor parameter; replace Settings.getDiscordInvite() with settings.get(...)
  • Update SettingsTest to use new Settings() directly