Repository: kroxylicious/kroxylicious
Pull request #608 (2023-09-11 04:15:35)

add Contributes and ConfigurationDefinition
### Type of change

- Enhancement / new feature
- Refactoring

### Description

Simplify how the Kroxylicious runtime interacts with services loaded at runtime.

### Additional Context



### Checklist

_Please go through this checklist and make sure all applicable tasks have been done_

- [x] Write tests
- [x] Make sure all tests pass
- [ ] Review performance test results. Ensure that any degradations to performance numbers are understood and justified.
- [ ] Make sure all Sonar-Lint warnings are addressed or are justifiably ignored.
- [ ] Update documentation
- [ ] Reference relevant issue(s) and close them after merging
- [ ] For user facing changes, update CHANGELOG.md (remember to include changes affecting the API of the test artefacts too).


sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; [![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=608)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=CODE_SMELL) [3 Code Smells](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=CODE_SMELL)

[![68.4%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '68.4%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=608&metric=new_coverage&view=list) [68.4% Coverage](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=608&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=608&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=608&metric=new_duplicated_lines_density&view=list)



![idea](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png 'idea') Catch issues before they fail your Quality Gate with our IDE extension ![sonarlint](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png 'sonarlint') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=sonarcloud-welcome)
sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=608)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=CODE_SMELL) [2 Code Smells](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=CODE_SMELL)

[![88.2%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '88.2%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=608&metric=new_coverage&view=list) [88.2% Coverage](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=608&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=608&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=608&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; [![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=608)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=CODE_SMELL) [2 Code Smells](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=608&resolved=false&types=CODE_SMELL)

[![70.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '70.0%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=608&metric=new_coverage&view=list) [70.0% Coverage](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=608&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=608&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=608&metric=new_duplicated_lines_density&view=list)



![idea](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png 'idea') Catch issues before they fail your Quality Gate with our IDE extension ![sonarlint](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png 'sonarlint') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=sonarcloud-welcome)
robobario: To catch some comments from our chat reviewing this:

I think removing the individual concrete Manager classes is nice, eliminating some boilerplate and class names that hurt the eye like `ClusterNetworkAddressConfigProviderContributorManager`. Now the steps to add another Contributor type will be to create the new Contributor interface and add some service metadata to the META-INF, then you can access it through ContributionManager :+1: 

It was a nice responsibility for the Manager classes to handle adapting the Service-specific construction Context and BaseConfig from the configuration file to create a combined Context. But the mechanics of handling that generically are a bit painful, I think it's a good enough trade-off to move that responsibility up to the client trying to obtain an instance (see FilterChainFactory).

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f087ce82..788086da 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ Please enumerate **all user-facing** changes using format `<githib issue/pr numb
 
 ## 0.3.0
 
+* [#608](https://github.com/kroxylicious/kroxylicious/pull/608): Improve the contributor API to allow it to express more properties about the configuration. This release deprecates `Contributor.getConfigType` in favour of `Contributor.getConfigDefinition`. It also removes the proliferation of ContributorManager classes by providing a single type which can handle all Contributors.
 * [#538](https://github.com/kroxylicious/kroxylicious/pull/538): Refactor FilterHandler and fix several bugs that would leave messages unflushed to client/broker.
 * [#531](https://github.com/kroxylicious/kroxylicious/pull/531): Simple Test Client now supports multi-RPC conversations with the server.
 * [#510](https://github.com/kroxylicious/kroxylicious/pull/510): Add multi-tenant kubernetes example
@@ -50,4 +51,4 @@ In the kroxylicious config, the brokerAddressPattern parameter for the PortPerBr
 present on the brokerAddressPattern parameter. Previously, it was accepted but would lead to a failure later.
 
 Kroxylicious configuration no longer requires a non empty `filters` list, users can leave it unset or configure in an empty
-list of filters and Kroxylicious will proxy to the cluster successfully.
+list of filters and Kroxylicious will proxy to the cluster successfully.
\ No newline at end of file
diff --git a/kroxylicious-api/src/main/java/io/kroxylicious/proxy/service/BaseContributor.java b/kroxylicious-api/src/main/java/io/kroxylicious/proxy/service/BaseContributor.java
index 3cceff4c..8e0c979a 100644
--- a/kroxylicious-api/src/main/java/io/kroxylicious/proxy/service/BaseContributor.java
+++ b/kroxylicious-api/src/main/java/io/kroxylicious/proxy/service/BaseContributor.java
@@ -20,35 +20,76 @@ import io.kroxylicious.proxy.config.BaseConfig;
  */
 public abstract class BaseContributor<T, S extends Context> implements Contributor<T, S> {
 
-    private final Map<String, InstanceBuilder<T, S>> shortNameToInstanceBuilder;
+    private final Map<String, ContributionDetails<T, S>> typeNameToInstanceBuilder;
 
     /**
      * Constructs and configures the contributor using the supplied {@code builder}.
      * @param builder builder
      */
     protected BaseContributor(BaseContributorBuilder<T, S> builder) {
-        shortNameToInstanceBuilder = builder.build();
+        typeNameToInstanceBuilder = builder.build();
     }
 
+    /**
+     * Allows consumers to identify if this contributor recognises the requested type.
+     * @param typeName The type name to test for.
+     * @return <code>true</code> if this Contributor provides the typeName.
+     */
+    @Override
+    public boolean contributes(String typeName) {
+        return typeNameToInstanceBuilder.containsKey(typeName);
+    }
+
+    /**
+     * Returns the details of configuration of the supplied typeName
+     * @param typeName the name of the type.
+     * @return the configuration definition for the typename
+     * @throws IllegalArgumentException if the type name is not known to this contributor
+     */
+    @Override
+    public ConfigurationDefinition getConfigDefinition(String typeName) {
+        if (typeNameToInstanceBuilder.containsKey(typeName)) {
+            return typeNameToInstanceBuilder.get(typeName).configurationDefinition();
+        }
+        else {
+            throw new IllegalArgumentException("No configuration definition registered for " + typeName);
+        }
+    }
+
+    /**
+     * @deprecated in favour of calling {@link #getConfigDefinition(String)}}
+     * @param typeName service short name
+     *
+     * @return {@code null} if the typename is not know to this contributor.
+     */
+    @SuppressWarnings("removal")
     @Override
-    public Class<? extends BaseConfig> getConfigType(String shortName) {
-        InstanceBuilder<T, S> instanceBuilder = shortNameToInstanceBuilder.get(shortName);
-        return instanceBuilder == null ? null : instanceBuilder.configClass;
+    @Deprecated(forRemoval = true, since = "0.3.0")
+    public Class<? extends BaseConfig> getConfigType(String typeName) {
+        if (typeNameToInstanceBuilder.containsKey(typeName)) {
+            return typeNameToInstanceBuilder.get(typeName).configurationDefinition().configurationType();
+        }
+        else {
+            return null;
+        }
     }
 
     @Override
-    public T getInstance(String shortName, S context) {
-        InstanceBuilder<T, S> instanceBuilder = shortNameToInstanceBuilder.get(shortName);
-        return instanceBuilder == null ? null : instanceBuilder.construct(context);
+    public T getInstance(String typeName, S context) {
+        if (typeNameToInstanceBuilder.containsKey(typeName)) {
+            InstanceBuilder<T, S> instanceBuilder = typeNameToInstanceBuilder.get(typeName).instanceBuilder();
+            return instanceBuilder == null ? null : instanceBuilder.construct(context);
+        }
+        else {
+            return null;
+        }
     }
 
     private static class InstanceBuilder<L, D extends Context> {
 
-        private final Class<? extends BaseConfig> configClass;
         private final Function<D, L> instanceFunction;
 
-        InstanceBuilder(Class<? extends BaseConfig> configClass, Function<D, L> instanceFunction) {
-            this.configClass = configClass;
+        InstanceBuilder(Function<D, L> instanceFunction) {
             this.instanceFunction = instanceFunction;
         }
 
@@ -57,7 +98,7 @@ public abstract class BaseContributor<T, S extends Context> implements Contribut
         }
 
         static <T extends BaseConfig, L, D extends Context> InstanceBuilder<L, D> builder(Class<T> configClass, BiFunction<D, T, L> instanceFunction) {
-            return new InstanceBuilder<>(configClass, context -> {
+            return new InstanceBuilder<>(context -> {
                 BaseConfig config = context.getConfig();
                 if (config == null) {
                     // tests pass in a null config, which some instance functions can tolerate
@@ -85,62 +126,63 @@ public abstract class BaseContributor<T, S extends Context> implements Contribut
         private BaseContributorBuilder() {
         }
 
-        private final Map<String, InstanceBuilder<L, D>> shortNameToInstanceBuilder = new HashMap<>();
+        private final Map<String, ContributionDetails<L, D>> typeNameToInstanceBuilder = new HashMap<>();
 
         /**
          * Registers a factory function for the construction of a service instance.
          *
-         * @param shortName service short name
+         * @param typeName service short name
          * @param configClass concrete type of configuration required by the service
          * @param instanceFunction function that constructs the service instance
          * @return this
          * @param <T> the configuration concrete type
          */
-        public <T extends BaseConfig> BaseContributorBuilder<L, D> add(String shortName, Class<T> configClass, Function<T, L> instanceFunction) {
-            return add(shortName, configClass, (context, config) -> instanceFunction.apply(config));
+        public <T extends BaseConfig> BaseContributorBuilder<L, D> add(String typeName, Class<T> configClass, Function<T, L> instanceFunction) {
+            return add(typeName, configClass, (context, config) -> instanceFunction.apply(config));
         }
 
         /**
          * Registers a factory function for the construction of a service instance.
          *
-         * @param shortName service short name
+         * @param typeName service short name
          * @param configClass concrete type of configuration required by the service
          * @param instanceFunction function that constructs the service instance
          * @return this
          * @param <T> the configuration concrete type
          */
-        public <T extends BaseConfig> BaseContributorBuilder<L, D> add(String shortName, Class<T> configClass, BiFunction<D, T, L> instanceFunction) {
-            if (shortNameToInstanceBuilder.containsKey(shortName)) {
-                throw new IllegalArgumentException(shortName + " already registered");
+        public <T extends BaseConfig> BaseContributorBuilder<L, D> add(String typeName, Class<T> configClass, BiFunction<D, T, L> instanceFunction) {
+            if (typeNameToInstanceBuilder.containsKey(typeName)) {
+                throw new IllegalArgumentException(typeName + " already registered");
             }
-            shortNameToInstanceBuilder.put(shortName, InstanceBuilder.builder(configClass, instanceFunction));
+            typeNameToInstanceBuilder.put(typeName,
+                    new ContributionDetails<>(new ConfigurationDefinition(configClass), InstanceBuilder.builder(configClass, instanceFunction)));
             return this;
         }
 
         /**
          * Registers a factory function for the construction of a service instance.
          *
-         * @param shortName service short name
+         * @param typeName service short name
          * @param instanceFunction function that constructs the service instance
          * @return this
          */
-        public BaseContributorBuilder<L, D> add(String shortName, Supplier<L> instanceFunction) {
-            return add(shortName, BaseConfig.class, config -> instanceFunction.get());
+        public BaseContributorBuilder<L, D> add(String typeName, Supplier<L> instanceFunction) {
+            return add(typeName, BaseConfig.class, config -> instanceFunction.get());
         }
 
         /**
          * Registers a factory function for the construction of a service instance.
          *
-         * @param shortName service short name
+         * @param typeName service short name
          * @param instanceFunction function that constructs the service instance from a context
          * @return this
          */
-        public BaseContributorBuilder<L, D> add(String shortName, Function<D, L> instanceFunction) {
-            return add(shortName, BaseConfig.class, (context, config) -> instanceFunction.apply(context));
+        public BaseContributorBuilder<L, D> add(String typeName, Function<D, L> instanceFunction) {
+            return add(typeName, BaseConfig.class, (context, config) -> instanceFunction.apply(context));
         }
 
-        Map<String, InstanceBuilder<L, D>> build() {
-            return Map.copyOf(shortNameToInstanceBuilder);
+        Map<String, ContributionDetails<L, D>> build() {
+            return Map.copyOf(typeNameToInstanceBuilder);
         }
     }
 
@@ -153,4 +195,6 @@ public abstract class BaseContributor<T, S extends Context> implements Contribut
     public static <L, D extends Context> BaseContributorBuilder<L, D> builder() {
         return new BaseContributorBuilder<>();
     }
-}
+
+    protected record ContributionDetails<T, D extends Context>(ConfigurationDefinition configurationDefinition, InstanceBuilder<T, D> instanceBuilder) {}
+}
\ No newline at end of file
diff --git a/kroxylicious-api/src/main/java/io/kroxylicious/proxy/service/ConfigurationDefinition.java b/kroxylicious-api/src/main/java/io/kroxylicious/proxy/service/ConfigurationDefinition.java
new file mode 100644
index 00000000..4579e388
--- /dev/null
+++ b/kroxylicious-api/src/main/java/io/kroxylicious/proxy/service/ConfigurationDefinition.java
@@ -0,0 +1,16 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.proxy.service;
+
+import io.kroxylicious.proxy.config.BaseConfig;
+
+/**
+ * Defines the details of how Contributions interact with configuration for {@link BaseContributor}'s purposes.
+ *
+ * @param configurationType defines the expected class for configuration objects
+ */
+public record ConfigurationDefinition(Class<? extends BaseConfig> configurationType) {}
\ No newline at end of file
diff --git a/kroxylicious-api/src/main/java/io/kroxylicious/proxy/service/Contributor.java b/kroxylicious-api/src/main/java/io/kroxylicious/proxy/service/Contributor.java
index d5913d63..ffbfcc89 100644
--- a/kroxylicious-api/src/main/java/io/kroxylicious/proxy/service/Contributor.java
+++ b/kroxylicious-api/src/main/java/io/kroxylicious/proxy/service/Contributor.java
@@ -16,19 +16,37 @@ import io.kroxylicious.proxy.config.BaseConfig;
 public interface Contributor<T, S extends Context> {
 
     /**
+     * Identifies if this Contributor instance can create instances for the provided short name.
+     * @param typeName The type name to test for.
+     * @return <code>true</code> if the <code>typeName</code> is supported by this contributor.
+     */
+    boolean contributes(String typeName);
+
+    /**
+     * @deprecated subsumed into {@link ConfigurationDefinition} and thus {@link Contributor#getConfigDefinition(String)}
+     *
      * Gets the concrete type of the configuration required by this service instance.
-     * @param shortName service short name
+     * @param typeName service short name
      *
      * @return class of a concrete type, or null if this contributor does not offer this short name.
      */
-    Class<? extends BaseConfig> getConfigType(String shortName);
+    @Deprecated(forRemoval = true, since = "0.3.0")
+    Class<? extends BaseConfig> getConfigType(String typeName);
+
+    /**
+     * Defines the configuration requirements of this contributor for the given short name.
+     * @param typeName the name of the type.
+     * @return the ConfigurationDefinition for the short name
+     */
+    ConfigurationDefinition getConfigDefinition(String typeName);
 
     /**
      * Creates an instance of the service.
      *
-     * @param shortName service short name
+     * @param typeName service short name
      * @param context   context containing service configuration which may be null if the service instance does not accept configuration.
      * @return the service instance, or null if this contributor does not offer this short name.
      */
-    T getInstance(String shortName, S context);
-}
+    T getInstance(String typeName, S context);
+
+}
\ No newline at end of file
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/bootstrap/FilterChainFactory.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/bootstrap/FilterChainFactory.java
index c36206f1..1eed9ec4 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/bootstrap/FilterChainFactory.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/bootstrap/FilterChainFactory.java
@@ -11,8 +11,9 @@ import java.util.Objects;
 import io.kroxylicious.proxy.config.Configuration;
 import io.kroxylicious.proxy.config.FilterDefinition;
 import io.kroxylicious.proxy.filter.FilterAndInvoker;
-import io.kroxylicious.proxy.internal.filter.FilterContributorManager;
+import io.kroxylicious.proxy.filter.FilterContributor;
 import io.kroxylicious.proxy.internal.filter.NettyFilterContext;
+import io.kroxylicious.proxy.service.ContributionManager;
 
 /**
  * Abstracts the creation of a chain of filter instances, hiding the configuration
@@ -34,16 +35,14 @@ public class FilterChainFactory {
      * @return the new chain.
      */
     public List<FilterAndInvoker> createFilters(NettyFilterContext context) {
-        FilterContributorManager filterContributorManager = FilterContributorManager.getInstance();
-
         List<FilterDefinition> filters = config.filters();
         if (filters == null || filters.isEmpty()) {
             return List.of();
         }
         return filters
                 .stream()
-                .map(f -> filterContributorManager.getFilter(f.type(), context, f.config()))
+                .map(f -> ContributionManager.INSTANCE.getInstance(FilterContributor.class, f.type(), context.wrap(f.config())))
                 .flatMap(filter -> FilterAndInvoker.build(filter).stream())
                 .toList();
     }
-}
+}
\ No newline at end of file
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/ClusterNetworkAddressConfigProviderTypeIdResolver.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/ClusterNetworkAddressConfigProviderTypeIdResolver.java
index 67b2cc2a..c3ba370c 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/ClusterNetworkAddressConfigProviderTypeIdResolver.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/ClusterNetworkAddressConfigProviderTypeIdResolver.java
@@ -12,7 +12,8 @@ import com.fasterxml.jackson.databind.DatabindContext;
 import com.fasterxml.jackson.databind.JavaType;
 import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase;
 
-import io.kroxylicious.proxy.internal.clusternetworkaddressconfigprovider.ClusterNetworkAddressConfigProviderContributorManager;
+import io.kroxylicious.proxy.clusternetworkaddressconfigprovider.ClusterNetworkAddressConfigProviderContributor;
+import io.kroxylicious.proxy.service.ContributionManager;
 
 public class ClusterNetworkAddressConfigProviderTypeIdResolver extends TypeIdResolverBase {
     private JavaType superType;
@@ -39,7 +40,7 @@ public class ClusterNetworkAddressConfigProviderTypeIdResolver extends TypeIdRes
 
     @Override
     public JavaType typeFromId(DatabindContext context, String id) throws IOException {
-        Class<?> subType = ClusterNetworkAddressConfigProviderContributorManager.getInstance().getConfigType(id);
-        return context.constructSpecializedType(superType, subType);
+        Class<? extends BaseConfig> result = ContributionManager.INSTANCE.getDefinition(ClusterNetworkAddressConfigProviderContributor.class, id).configurationType();
+        return context.constructSpecializedType(superType, result);
     }
-}
+}
\ No newline at end of file
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/FilterConfigTypeIdResolver.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/FilterConfigTypeIdResolver.java
index 3f43afdf..caf2e9da 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/FilterConfigTypeIdResolver.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/FilterConfigTypeIdResolver.java
@@ -12,7 +12,8 @@ import com.fasterxml.jackson.databind.DatabindContext;
 import com.fasterxml.jackson.databind.JavaType;
 import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase;
 
-import io.kroxylicious.proxy.internal.filter.FilterContributorManager;
+import io.kroxylicious.proxy.filter.FilterContributor;
+import io.kroxylicious.proxy.service.ContributionManager;
 
 public class FilterConfigTypeIdResolver extends TypeIdResolverBase {
     private JavaType superType;
@@ -39,7 +40,8 @@ public class FilterConfigTypeIdResolver extends TypeIdResolverBase {
 
     @Override
     public JavaType typeFromId(DatabindContext context, String id) throws IOException {
-        Class<?> subType = FilterContributorManager.getInstance().getConfigType(id);
+        Class<?> subType = ContributionManager.INSTANCE.getDefinition(FilterContributor.class, id)
+                .configurationType();
         return context.constructSpecializedType(superType, subType);
     }
-}
+}
\ No newline at end of file
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/MicrometerHookConfigTypeIdResolver.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/MicrometerHookConfigTypeIdResolver.java
index c57fd7b9..c9ea6248 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/MicrometerHookConfigTypeIdResolver.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/MicrometerHookConfigTypeIdResolver.java
@@ -12,7 +12,8 @@ import com.fasterxml.jackson.databind.DatabindContext;
 import com.fasterxml.jackson.databind.JavaType;
 import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase;
 
-import io.kroxylicious.proxy.micrometer.MicrometerConfigurationHookContributorManager;
+import io.kroxylicious.proxy.micrometer.MicrometerConfigurationHookContributor;
+import io.kroxylicious.proxy.service.ContributionManager;
 
 public class MicrometerHookConfigTypeIdResolver extends TypeIdResolverBase {
     private JavaType superType;
@@ -39,7 +40,7 @@ public class MicrometerHookConfigTypeIdResolver extends TypeIdResolverBase {
 
     @Override
     public JavaType typeFromId(DatabindContext context, String id) throws IOException {
-        Class<?> subType = MicrometerConfigurationHookContributorManager.getInstance().getConfigType(id);
-        return context.constructSpecializedType(superType, subType);
+        Class<? extends BaseConfig> result = ContributionManager.INSTANCE.getDefinition(MicrometerConfigurationHookContributor.class, id).configurationType();
+        return context.constructSpecializedType(superType, result);
     }
-}
+}
\ No newline at end of file
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/VirtualCluster.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/VirtualCluster.java
index 3101badd..675eaba6 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/VirtualCluster.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/VirtualCluster.java
@@ -9,9 +9,12 @@ import java.util.Optional;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 
+import io.kroxylicious.proxy.clusternetworkaddressconfigprovider.ClusterNetworkAddressConfigProviderContributor;
 import io.kroxylicious.proxy.config.tls.Tls;
-import io.kroxylicious.proxy.internal.clusternetworkaddressconfigprovider.ClusterNetworkAddressConfigProviderContributorManager;
 import io.kroxylicious.proxy.service.ClusterNetworkAddressConfigProvider;
+import io.kroxylicious.proxy.service.ContributionManager;
+
+import static io.kroxylicious.proxy.service.Context.wrap;
 
 public record VirtualCluster(TargetCluster targetCluster,
                              @JsonProperty(required = true) ClusterNetworkAddressConfigProviderDefinition clusterNetworkAddressConfigProvider,
@@ -28,7 +31,8 @@ public record VirtualCluster(TargetCluster targetCluster,
     }
 
     private ClusterNetworkAddressConfigProvider toClusterNetworkAddressConfigProviderModel() {
-        return ClusterNetworkAddressConfigProviderContributorManager.getInstance()
-                .getClusterEndpointConfigProvider(clusterNetworkAddressConfigProvider().type(), clusterNetworkAddressConfigProvider().config());
+        String shortName = clusterNetworkAddressConfigProvider().type();
+        return ContributionManager.INSTANCE.getInstance(
+                ClusterNetworkAddressConfigProviderContributor.class, shortName, wrap(this.clusterNetworkAddressConfigProvider().config()));
     }
-}
+}
\ No newline at end of file
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/MeterRegistries.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/MeterRegistries.java
index c922bb72..c3c022d7 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/MeterRegistries.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/MeterRegistries.java
@@ -23,7 +23,10 @@ import io.micrometer.prometheus.PrometheusMeterRegistry;
 
 import io.kroxylicious.proxy.config.MicrometerDefinition;
 import io.kroxylicious.proxy.micrometer.MicrometerConfigurationHook;
-import io.kroxylicious.proxy.micrometer.MicrometerConfigurationHookContributorManager;
+import io.kroxylicious.proxy.micrometer.MicrometerConfigurationHookContributor;
+import io.kroxylicious.proxy.service.ContributionManager;
+
+import static io.kroxylicious.proxy.service.Context.wrap;
 
 public class MeterRegistries implements AutoCloseable {
     private final PrometheusMeterRegistry prometheusMeterRegistry;
@@ -40,10 +43,11 @@ public class MeterRegistries implements AutoCloseable {
     private List<MicrometerConfigurationHook> registerHooks(List<MicrometerDefinition> micrometerConfig) {
         CompositeMeterRegistry globalRegistry = Metrics.globalRegistry;
         preventDifferentTagNameRegistration(globalRegistry);
-        MicrometerConfigurationHookContributorManager manager = MicrometerConfigurationHookContributorManager.getInstance();
-        var hooks = micrometerConfig.stream().map(f -> manager.getHook(f.type(), f.config())).toList();
-        hooks.forEach(micrometerConfigurationHook -> micrometerConfigurationHook.configure(globalRegistry));
-        return hooks;
+        var configurationHooks = micrometerConfig.stream()
+                .map(f -> ContributionManager.INSTANCE.getInstance(MicrometerConfigurationHookContributor.class, f.type(), wrap(f.config())))
+                .toList();
+        configurationHooks.forEach(micrometerConfigurationHook -> micrometerConfigurationHook.configure(globalRegistry));
+        return configurationHooks;
     }
 
     /**
@@ -54,7 +58,8 @@ public class MeterRegistries implements AutoCloseable {
      *
      * @param registry registry
      */
-    /* test */ static void preventDifferentTagNameRegistration(CompositeMeterRegistry registry) {
+    /* test */
+    static void preventDifferentTagNameRegistration(CompositeMeterRegistry registry) {
         registry.config().meterFilter(new MeterFilter() {
             @Override
             public MeterFilterReply accept(Meter.Id id) {
@@ -90,4 +95,4 @@ public class MeterRegistries implements AutoCloseable {
         copy.forEach(Metrics.globalRegistry::remove);
         Metrics.removeRegistry(prometheusMeterRegistry);
     }
-}
+}
\ No newline at end of file
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/ClusterNetworkAddressConfigProviderContributorManager.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/ClusterNetworkAddressConfigProviderContributorManager.java
deleted file mode 100644
index 0d4b4a77..00000000
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/ClusterNetworkAddressConfigProviderContributorManager.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright Kroxylicious Authors.
- *
- * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
- */
-package io.kroxylicious.proxy.internal.clusternetworkaddressconfigprovider;
-
-import java.util.Iterator;
-import java.util.ServiceLoader;
-
-import io.kroxylicious.proxy.clusternetworkaddressconfigprovider.ClusterNetworkAddressConfigProviderContributor;
-import io.kroxylicious.proxy.config.BaseConfig;
-import io.kroxylicious.proxy.service.ClusterNetworkAddressConfigProvider;
-
-import static io.kroxylicious.proxy.service.Context.wrap;
-
-public class ClusterNetworkAddressConfigProviderContributorManager {
-
-    private static final ClusterNetworkAddressConfigProviderContributorManager INSTANCE = new ClusterNetworkAddressConfigProviderContributorManager();
-
-    private final ServiceLoader<ClusterNetworkAddressConfigProviderContributor> contributors;
-
-    private ClusterNetworkAddressConfigProviderContributorManager() {
-        this.contributors = ServiceLoader.load(ClusterNetworkAddressConfigProviderContributor.class);
-    }
-
-    public static ClusterNetworkAddressConfigProviderContributorManager getInstance() {
-        return INSTANCE;
-    }
-
-    public Class<? extends BaseConfig> getConfigType(String shortName) {
-        Iterator<ClusterNetworkAddressConfigProviderContributor> it = contributors.iterator();
-        while (it.hasNext()) {
-            ClusterNetworkAddressConfigProviderContributor contributor = it.next();
-            Class<? extends BaseConfig> configType = contributor.getConfigType(shortName);
-            if (configType != null) {
-                return configType;
-            }
-        }
-
-        throw new IllegalArgumentException("No endpoint provider found for name '" + shortName + "'");
-    }
-
-    public ClusterNetworkAddressConfigProvider getClusterEndpointConfigProvider(String shortName, BaseConfig baseConfig) {
-        for (ClusterNetworkAddressConfigProviderContributor contributor : contributors) {
-            var assigner = contributor.getInstance(shortName, wrap(baseConfig));
-            if (assigner != null) {
-                return assigner;
-            }
-        }
-
-        throw new IllegalArgumentException("No endpoint provider found for name '" + shortName + "'");
-    }
-}
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/filter/FilterContributorManager.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/filter/FilterContributorManager.java
deleted file mode 100644
index f53477fb..00000000
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/filter/FilterContributorManager.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright Kroxylicious Authors.
- *
- * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
- */
-package io.kroxylicious.proxy.internal.filter;
-
-import java.util.Iterator;
-import java.util.ServiceLoader;
-
-import io.kroxylicious.proxy.config.BaseConfig;
-import io.kroxylicious.proxy.filter.Filter;
-import io.kroxylicious.proxy.filter.FilterConstructContext;
-import io.kroxylicious.proxy.filter.FilterContributor;
-
-public class FilterContributorManager {
-
-    private static final FilterContributorManager INSTANCE = new FilterContributorManager();
-
-    private final ServiceLoader<FilterContributor> contributors;
-
-    private FilterContributorManager() {
-        this.contributors = ServiceLoader.load(FilterContributor.class);
-    }
-
-    public static FilterContributorManager getInstance() {
-        return INSTANCE;
-    }
-
-    public Class<? extends BaseConfig> getConfigType(String shortName) {
-        Iterator<FilterContributor> it = contributors.iterator();
-        while (it.hasNext()) {
-            FilterContributor contributor = it.next();
-            Class<? extends BaseConfig> configType = contributor.getConfigType(shortName);
-            if (configType != null) {
-                return configType;
-            }
-        }
-
-        throw new IllegalArgumentException("No filter found for name '" + shortName + "'");
-    }
-
-    public Filter getFilter(String shortName, NettyFilterContext context, BaseConfig filterConfig) {
-        Iterator<FilterContributor> it = contributors.iterator();
-        while (it.hasNext()) {
-            FilterContributor contributor = it.next();
-            FilterConstructContext context1 = context.wrap(filterConfig);
-            Filter filter = contributor.getInstance(shortName, context1);
-            if (filter != null) {
-                return filter;
-            }
-        }
-
-        throw new IllegalArgumentException("No filter found for name '" + shortName + "'");
-    }
-
-}
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/filter/NettyFilterContext.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/filter/NettyFilterContext.java
index e24f5f44..71b22c5b 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/filter/NettyFilterContext.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/filter/NettyFilterContext.java
@@ -19,7 +19,7 @@ public class NettyFilterContext {
         this.eventLoop = eventLoop;
     }
 
-    FilterConstructContext wrap(BaseConfig config) {
+    public FilterConstructContext wrap(BaseConfig config) {
         return new NettyFilterConfigContext(config);
     }
 
@@ -40,4 +40,4 @@ public class NettyFilterContext {
             return filterConfig;
         }
     }
-}
+}
\ No newline at end of file
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/micrometer/MicrometerConfigurationHookContributorManager.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/micrometer/MicrometerConfigurationHookContributorManager.java
deleted file mode 100644
index f0d081e2..00000000
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/micrometer/MicrometerConfigurationHookContributorManager.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright Kroxylicious Authors.
- *
- * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
- */
-package io.kroxylicious.proxy.micrometer;
-
-import java.util.ServiceLoader;
-
-import io.kroxylicious.proxy.config.BaseConfig;
-
-import static io.kroxylicious.proxy.service.Context.wrap;
-
-public class MicrometerConfigurationHookContributorManager {
-
-    private static final MicrometerConfigurationHookContributorManager INSTANCE = new MicrometerConfigurationHookContributorManager();
-
-    private final ServiceLoader<MicrometerConfigurationHookContributor> contributors;
-
-    public static MicrometerConfigurationHookContributorManager getInstance() {
-        return INSTANCE;
-    }
-
-    private MicrometerConfigurationHookContributorManager() {
-        this.contributors = ServiceLoader.load(MicrometerConfigurationHookContributor.class);
-    }
-
-    public Class<? extends BaseConfig> getConfigType(String shortName) {
-        for (MicrometerConfigurationHookContributor contributor : contributors) {
-            Class<? extends BaseConfig> configType = contributor.getConfigType(shortName);
-            if (configType != null) {
-                return configType;
-            }
-        }
-
-        throw new IllegalArgumentException("No micrometer configuration hook found for name '" + shortName + "'");
-    }
-
-    public MicrometerConfigurationHook getHook(String shortName, BaseConfig filterConfig) {
-        for (MicrometerConfigurationHookContributor contributor : contributors) {
-            MicrometerConfigurationHook hook = contributor.getInstance(shortName, wrap(filterConfig));
-            if (hook != null) {
-                return hook;
-            }
-        }
-
-        throw new IllegalArgumentException("No micrometer configuration hook found for name '" + shortName + "'");
-    }
-
-}
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/service/ContributionManager.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/service/ContributionManager.java
new file mode 100644
index 00000000..941346e3
--- /dev/null
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/service/ContributionManager.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.proxy.service;
+
+import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+@SuppressWarnings({ "rawtypes", "unchecked" })
+public class ContributionManager {
+    public static final ContributionManager INSTANCE = new ContributionManager();
+
+    private final Map<Class, Iterable> contributors;
+    private final Function<Class, Iterable> loaderFunction;
+
+    private ContributionManager() {
+        this(ServiceLoader::load);
+    }
+
+    /* test */ ContributionManager(Function<Class, Iterable> loaderFunction) {
+        this.contributors = new ConcurrentHashMap<>();
+        this.loaderFunction = loaderFunction;
+    }
+
+    public <T, C extends Context, S extends Contributor<T, C>> ConfigurationDefinition getDefinition(Class<S> contributorClass, String typeName) {
+        return findContributor(contributorClass, typeName, (typName, contributor) -> contributor.getConfigDefinition(typName));
+    }
+
+    public <T, C extends Context, S extends Contributor<T, C>> T getInstance(Class<S> contributorClass, String typeName, C constructionContext) {
+        return findContributor(contributorClass, typeName, (typName, contributor) -> contributor.getInstance(typeName, constructionContext));
+    }
+
+    private <T, C extends Context, S extends Contributor<T, C>, X> X findContributor(Class<S> contributorClass, String typeName, BiFunction<String, S, X> extractor) {
+        final Iterable<S> contributorsForClass = this.contributors.computeIfAbsent(contributorClass, loaderFunction);
+        for (S contributor : contributorsForClass) {
+            if (contributor.contributes(typeName)) {
+                return extractor.apply(typeName, contributor);
+            }
+        }
+        throw new IllegalArgumentException("Name '" + typeName + "' is not contributed by any " + contributorClass);
+    }
+}
\ No newline at end of file


Test patch:
diff --git a/kroxylicious-additional-filters/kroxylicious-multitenant/src/test/java/io/kroxylicious/proxy/filter/multitenant/MultiTenantFilterContributorTest.java b/kroxylicious-additional-filters/kroxylicious-multitenant/src/test/java/io/kroxylicious/proxy/filter/multitenant/MultiTenantFilterContributorTest.java
index ab48ca46..a07c104e 100644
--- a/kroxylicious-additional-filters/kroxylicious-multitenant/src/test/java/io/kroxylicious/proxy/filter/multitenant/MultiTenantFilterContributorTest.java
+++ b/kroxylicious-additional-filters/kroxylicious-multitenant/src/test/java/io/kroxylicious/proxy/filter/multitenant/MultiTenantFilterContributorTest.java
@@ -12,6 +12,7 @@ import org.mockito.Mockito;
 import io.kroxylicious.proxy.config.BaseConfig;
 import io.kroxylicious.proxy.filter.Filter;
 import io.kroxylicious.proxy.filter.FilterConstructContext;
+import io.kroxylicious.proxy.service.ConfigurationDefinition;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -24,6 +25,13 @@ class MultiTenantFilterContributorTest {
         assertThat(configType).isEqualTo(BaseConfig.class);
     }
 
+    @Test
+    void testGetConfigTypeViaConfigurationDefinition() {
+        MultiTenantFilterContributor contributor = new MultiTenantFilterContributor();
+        ConfigurationDefinition actualConfigurationDefinition = contributor.getConfigDefinition("MultiTenant");
+        assertThat(actualConfigurationDefinition).isNotNull().hasFieldOrPropertyWithValue("configurationType", BaseConfig.class);
+    }
+
     @Test
     void testGetInstance() {
         MultiTenantFilterContributor contributor = new MultiTenantFilterContributor();
@@ -31,4 +39,4 @@ class MultiTenantFilterContributorTest {
         assertThat(filter).isNotNull().isInstanceOf(MultiTenantTransformationFilter.class);
     }
 
-}
+}
\ No newline at end of file
diff --git a/kroxylicious-additional-filters/kroxylicious-schema-validation/src/test/java/io/kroxylicious/proxy/filter/schema/ProduceRequestValidationFilterContributorTest.java b/kroxylicious-additional-filters/kroxylicious-schema-validation/src/test/java/io/kroxylicious/proxy/filter/schema/ProduceRequestValidationFilterContributorTest.java
index 166c8c74..da1790aa 100644
--- a/kroxylicious-additional-filters/kroxylicious-schema-validation/src/test/java/io/kroxylicious/proxy/filter/schema/ProduceRequestValidationFilterContributorTest.java
+++ b/kroxylicious-additional-filters/kroxylicious-schema-validation/src/test/java/io/kroxylicious/proxy/filter/schema/ProduceRequestValidationFilterContributorTest.java
@@ -16,6 +16,7 @@ import io.kroxylicious.proxy.filter.Filter;
 import io.kroxylicious.proxy.filter.FilterConstructContext;
 import io.kroxylicious.proxy.filter.schema.config.RecordValidationRule;
 import io.kroxylicious.proxy.filter.schema.config.ValidationConfig;
+import io.kroxylicious.proxy.service.ConfigurationDefinition;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -28,6 +29,13 @@ class ProduceRequestValidationFilterContributorTest {
         assertThat(configType).isEqualTo(ValidationConfig.class);
     }
 
+    @Test
+    void testGetConfigTypeViaConfigurationDefinition() {
+        ProduceRequestValidationFilterContributor contributor = new ProduceRequestValidationFilterContributor();
+        ConfigurationDefinition actualConfigurationDefinition = contributor.getConfigDefinition("ProduceValidator");
+        assertThat(actualConfigurationDefinition).isNotNull().hasFieldOrPropertyWithValue("configurationType", ValidationConfig.class);
+    }
+
     @Test
     void testGetInstance() {
         ProduceRequestValidationFilterContributor contributor = new ProduceRequestValidationFilterContributor();
diff --git a/kroxylicious-api/src/test/java/io/kroxylicious/proxy/service/BaseContributorTest.java b/kroxylicious-api/src/test/java/io/kroxylicious/proxy/service/BaseContributorTest.java
index a5100c54..27420552 100644
--- a/kroxylicious-api/src/test/java/io/kroxylicious/proxy/service/BaseContributorTest.java
+++ b/kroxylicious-api/src/test/java/io/kroxylicious/proxy/service/BaseContributorTest.java
@@ -34,6 +34,16 @@ class BaseContributorTest {
         assertThat(one).isEqualTo(BaseConfig.class);
     }
 
+    @Test
+    void testDefaultConfigClassFromConfigDefinition() {
+        BaseContributor.BaseContributorBuilder<Long, Context> builder = BaseContributor.builder();
+        builder.add("one", () -> 1L);
+        BaseContributor<Long, Context> baseContributor = new BaseContributor<>(builder) {
+        };
+        ConfigurationDefinition one = baseContributor.getConfigDefinition("one");
+        assertThat(one).hasFieldOrPropertyWithValue("configurationType", BaseConfig.class);
+    }
+
     @Test
     void testSupplier() {
         BaseContributor.BaseContributorBuilder<Long, Context> builder = BaseContributor.builder();
@@ -80,6 +90,16 @@ class BaseContributorTest {
         assertThat(configType).isEqualTo(LongConfig.class);
     }
 
+    @Test
+    void testSpecifyingConfigTypeViaConfigDefinition() {
+        BaseContributor.BaseContributorBuilder<Long, Context> builder = BaseContributor.builder();
+        builder.add("fromBaseConfig", LongConfig.class, baseConfig -> baseConfig.value);
+        BaseContributor<Long, Context> baseContributor = new BaseContributor<>(builder) {
+        };
+        ConfigurationDefinition actualConfigDefinition = baseContributor.getConfigDefinition("fromBaseConfig");
+        assertThat(actualConfigDefinition).hasFieldOrPropertyWithValue("configurationType", LongConfig.class);
+    }
+
     @Test
     void testSpecifyingConfigTypeInstance() {
         BaseContributor.BaseContributorBuilder<Long, Context> builder = BaseContributor.builder();
@@ -112,10 +132,36 @@ class BaseContributorTest {
         builder.add("fromBaseConfig", LongConfig.class, baseConfig -> baseConfig.value);
         BaseContributor<Long, Context> baseContributor = new BaseContributor<>(builder) {
         };
-        AnotherConfig incompatibleConfig = new AnotherConfig();
-        assertThatThrownBy(() -> {
-            baseContributor.getInstance("fromBaseConfig", wrap(incompatibleConfig));
-        }).isInstanceOf(IllegalArgumentException.class);
+        final Context wrappedContext = wrap(new AnotherConfig());
+
+        assertThatThrownBy(() -> baseContributor.getInstance("fromBaseConfig", wrappedContext)).isInstanceOf(IllegalArgumentException.class);
     }
 
-}
+    @Test
+    void shouldGetConfigDefinitionForKnownTypeName() {
+        // Given
+        BaseContributor.BaseContributorBuilder<Long, Context> builder = BaseContributor.builder();
+        builder.add("fromBaseConfig", LongConfig.class, baseConfig -> baseConfig.value);
+        BaseContributor<Long, Context> baseContributor = new BaseContributor<>(builder) {
+        };
+
+        // When
+        ConfigurationDefinition actual = baseContributor.getConfigDefinition("fromBaseConfig");
+
+        // Then
+        assertThat(actual).isNotNull().hasFieldOrPropertyWithValue("configurationType", LongConfig.class);
+    }
+
+    @Test
+    void shouldThrowForUnknownTypeNameConfigDefinition() {
+        // Given
+        BaseContributor.BaseContributorBuilder<Long, Context> builder = BaseContributor.builder();
+        BaseContributor<Long, Context> baseContributor = new BaseContributor<>(builder) {
+        };
+
+        // When
+        assertThatThrownBy(() -> baseContributor.getConfigDefinition("fromBaseConfig")).isInstanceOf(IllegalArgumentException.class);
+
+        // Then
+    }
+}
\ No newline at end of file
diff --git a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/config/ClusterNetworkAddressConfigProviderDefinitionBuilder.java b/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/config/ClusterNetworkAddressConfigProviderDefinitionBuilder.java
index 7ee676d5..5f712e33 100644
--- a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/config/ClusterNetworkAddressConfigProviderDefinitionBuilder.java
+++ b/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/config/ClusterNetworkAddressConfigProviderDefinitionBuilder.java
@@ -8,7 +8,8 @@ package io.kroxylicious.proxy.config;
 
 import java.util.Map;
 
-import io.kroxylicious.proxy.internal.clusternetworkaddressconfigprovider.ClusterNetworkAddressConfigProviderContributorManager;
+import io.kroxylicious.proxy.clusternetworkaddressconfigprovider.ClusterNetworkAddressConfigProviderContributor;
+import io.kroxylicious.proxy.service.ContributionManager;
 
 public class ClusterNetworkAddressConfigProviderDefinitionBuilder extends AbstractDefinitionBuilder<ClusterNetworkAddressConfigProviderDefinition> {
     public ClusterNetworkAddressConfigProviderDefinitionBuilder(String type) {
@@ -17,7 +18,8 @@ public class ClusterNetworkAddressConfigProviderDefinitionBuilder extends Abstra
 
     @Override
     protected ClusterNetworkAddressConfigProviderDefinition buildInternal(String type, Map<String, Object> config) {
-        var configType = ClusterNetworkAddressConfigProviderContributorManager.getInstance().getConfigType(type);
-        return new ClusterNetworkAddressConfigProviderDefinition(type, mapper.convertValue(config, configType));
+        Class<? extends BaseConfig> result = ContributionManager.INSTANCE.getDefinition(ClusterNetworkAddressConfigProviderContributor.class, type).configurationType();
+
+        return new ClusterNetworkAddressConfigProviderDefinition(type, mapper.convertValue(config, result));
     }
-}
+}
\ No newline at end of file
diff --git a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/config/FilterDefinitionBuilder.java b/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/config/FilterDefinitionBuilder.java
index 49e7034d..e12ba245 100644
--- a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/config/FilterDefinitionBuilder.java
+++ b/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/config/FilterDefinitionBuilder.java
@@ -8,7 +8,8 @@ package io.kroxylicious.proxy.config;
 
 import java.util.Map;
 
-import io.kroxylicious.proxy.internal.filter.FilterContributorManager;
+import io.kroxylicious.proxy.filter.FilterContributor;
+import io.kroxylicious.proxy.service.ContributionManager;
 
 public class FilterDefinitionBuilder extends AbstractDefinitionBuilder<FilterDefinition> {
     public FilterDefinitionBuilder(String type) {
@@ -17,7 +18,8 @@ public class FilterDefinitionBuilder extends AbstractDefinitionBuilder<FilterDef
 
     @Override
     protected FilterDefinition buildInternal(String type, Map<String, Object> config) {
-        var configType = FilterContributorManager.getInstance().getConfigType(type);
+        var configType = ContributionManager.INSTANCE.getDefinition(FilterContributor.class, type)
+                .configurationType();
         return new FilterDefinition(type, mapper.convertValue(config, configType));
     }
-}
+}
\ No newline at end of file
diff --git a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/config/MicrometerDefinitionBuilder.java b/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/config/MicrometerDefinitionBuilder.java
index 1337d9fb..bd481c6f 100644
--- a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/config/MicrometerDefinitionBuilder.java
+++ b/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/config/MicrometerDefinitionBuilder.java
@@ -8,7 +8,8 @@ package io.kroxylicious.proxy.config;
 
 import java.util.Map;
 
-import io.kroxylicious.proxy.micrometer.MicrometerConfigurationHookContributorManager;
+import io.kroxylicious.proxy.micrometer.MicrometerConfigurationHookContributor;
+import io.kroxylicious.proxy.service.ContributionManager;
 
 public class MicrometerDefinitionBuilder extends AbstractDefinitionBuilder<MicrometerDefinition> {
     public MicrometerDefinitionBuilder(String type) {
@@ -17,7 +18,8 @@ public class MicrometerDefinitionBuilder extends AbstractDefinitionBuilder<Micro
 
     @Override
     protected MicrometerDefinition buildInternal(String type, Map<String, Object> config) {
-        var configType = MicrometerConfigurationHookContributorManager.getInstance().getConfigType(type);
-        return new MicrometerDefinition(type, mapper.convertValue(config, configType));
+        Class<? extends BaseConfig> result = ContributionManager.INSTANCE.getDefinition(MicrometerConfigurationHookContributor.class, type).configurationType();
+
+        return new MicrometerDefinition(type, mapper.convertValue(config, result));
     }
-}
+}
\ No newline at end of file
diff --git a/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/ClusterNetworkAddressConfigProviderContributorManagerTest.java b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/ClusterNetworkAddressConfigProviderContributorManagerTest.java
deleted file mode 100644
index 5fcc63ef..00000000
--- a/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/ClusterNetworkAddressConfigProviderContributorManagerTest.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright Kroxylicious Authors.
- *
- * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
- */
-
-package io.kroxylicious.proxy.internal.clusternetworkaddressconfigprovider;
-
-import org.junit.jupiter.api.Test;
-
-import io.kroxylicious.proxy.config.BaseConfig;
-import io.kroxylicious.proxy.service.ClusterNetworkAddressConfigProvider;
-
-import static io.kroxylicious.proxy.internal.clusternetworkaddressconfigprovider.TestClusterNetworkAddressConfigProviderContributor.SHORT_NAME;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-
-class ClusterNetworkAddressConfigProviderContributorManagerTest {
-
-    public static final ClusterNetworkAddressConfigProviderContributorManager INSTANCE = ClusterNetworkAddressConfigProviderContributorManager.getInstance();
-
-    @Test
-    void testGetConfigType() {
-        Class<? extends BaseConfig> clazz = INSTANCE.getConfigType(SHORT_NAME);
-        assertThat(clazz).isEqualTo(Config.class);
-    }
-
-    @Test
-    void testGetConfigTypeFailsWhenShortnameHasNoMatches() {
-        assertThatThrownBy(() -> INSTANCE.getConfigType("mismatch")).isInstanceOf(IllegalArgumentException.class)
-                .hasMessageContaining("No endpoint provider found for name 'mismatch'");
-    }
-
-    @Test
-    void testGetClusterEndpointProvider() {
-        Config config = new Config();
-        ClusterNetworkAddressConfigProvider provider = INSTANCE.getClusterEndpointConfigProvider(SHORT_NAME, config);
-        assertThat(provider).isInstanceOf(TestClusterNetworkAddressConfigProvider.class);
-        TestClusterNetworkAddressConfigProvider testProvider = (TestClusterNetworkAddressConfigProvider) provider;
-        assertThat(testProvider.config()).isSameAs(config);
-        assertThat(testProvider.shortName()).isEqualTo(SHORT_NAME);
-        assertThat(testProvider.context().getConfig()).isEqualTo(config);
-    }
-
-    @Test
-    void testGetClusterEndpointProviderFailsWhenShortnameHasNoMatches() {
-        Config config = new Config();
-        assertThatThrownBy(() -> INSTANCE.getClusterEndpointConfigProvider("mismatch", config))
-                .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("No endpoint provider found for name 'mismatch'");
-    }
-
-}
\ No newline at end of file
diff --git a/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/TestClusterNetworkAddressConfigProviderContributor.java b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/TestClusterNetworkAddressConfigProviderContributor.java
index a0c1d84f..1b5f6d38 100644
--- a/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/TestClusterNetworkAddressConfigProviderContributor.java
+++ b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/TestClusterNetworkAddressConfigProviderContributor.java
@@ -11,12 +11,18 @@ import java.util.Objects;
 import io.kroxylicious.proxy.clusternetworkaddressconfigprovider.ClusterNetworkAddressConfigProviderContributor;
 import io.kroxylicious.proxy.config.BaseConfig;
 import io.kroxylicious.proxy.service.ClusterNetworkAddressConfigProvider;
+import io.kroxylicious.proxy.service.ConfigurationDefinition;
 import io.kroxylicious.proxy.service.Context;
 
 public class TestClusterNetworkAddressConfigProviderContributor implements ClusterNetworkAddressConfigProviderContributor {
 
     public static final String SHORT_NAME = "test";
 
+    @Override
+    public boolean contributes(String shortName) {
+        return Objects.equals(shortName, SHORT_NAME);
+    }
+
     @Override
     public Class<? extends BaseConfig> getConfigType(String shortName) {
         if (!Objects.equals(shortName, SHORT_NAME)) {
@@ -25,6 +31,14 @@ public class TestClusterNetworkAddressConfigProviderContributor implements Clust
         return Config.class;
     }
 
+    @Override
+    public ConfigurationDefinition getConfigDefinition(String shortName) {
+        if (Objects.equals(shortName, SHORT_NAME)) {
+            return new ConfigurationDefinition(Config.class);
+        }
+        return null;
+    }
+
     @Override
     public ClusterNetworkAddressConfigProvider getInstance(String shortName, Context context) {
         if (!Objects.equals(shortName, SHORT_NAME)) {
@@ -32,4 +46,4 @@ public class TestClusterNetworkAddressConfigProviderContributor implements Clust
         }
         return new TestClusterNetworkAddressConfigProvider(shortName, context.getConfig(), context);
     }
-}
+}
\ No newline at end of file
diff --git a/kroxylicious/src/test/java/io/kroxylicious/proxy/micrometer/MicrometerConfigurationHookContributorManagerTest.java b/kroxylicious/src/test/java/io/kroxylicious/proxy/micrometer/MicrometerConfigurationHookContributorManagerTest.java
deleted file mode 100644
index 9b46afd0..00000000
--- a/kroxylicious/src/test/java/io/kroxylicious/proxy/micrometer/MicrometerConfigurationHookContributorManagerTest.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright Kroxylicious Authors.
- *
- * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
- */
-
-package io.kroxylicious.proxy.micrometer;
-
-import org.junit.jupiter.api.Test;
-
-import io.kroxylicious.proxy.config.BaseConfig;
-import io.kroxylicious.proxy.micrometer.TestMicrometerConfigurationHookContributor.Config;
-
-import static io.kroxylicious.proxy.micrometer.TestMicrometerConfigurationHookContributor.SHORT_NAME;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-
-class MicrometerConfigurationHookContributorManagerTest {
-
-    public static final MicrometerConfigurationHookContributorManager INSTANCE = MicrometerConfigurationHookContributorManager.getInstance();
-
-    @Test
-    void testGetConfigType() {
-        Class<? extends BaseConfig> clazz = INSTANCE.getConfigType(SHORT_NAME);
-        assertThat(clazz).isEqualTo(Config.class);
-    }
-
-    @Test
-    void testGetConfigTypeWhenNoContributorMatchesShortName() {
-        assertThatThrownBy(() -> INSTANCE.getConfigType("mismatched"))
-                .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("No micrometer configuration hook found for name 'mismatched'");
-    }
-
-    @Test
-    void testGetHook() {
-        Config config = new Config();
-        MicrometerConfigurationHook hook = INSTANCE.getHook(SHORT_NAME, config);
-        assertThat(hook).isInstanceOf(TestHook.class);
-        TestHook hook1 = (TestHook) hook;
-        assertThat(hook1.shortName()).isEqualTo(SHORT_NAME);
-        assertThat(hook1.context().getConfig()).isSameAs(config);
-        assertThat(hook1.config()).isSameAs(config);
-    }
-
-    @Test
-    void testGetHookTypeWhenNoContributorMatchesShortName() {
-        Config config = new Config();
-        assertThatThrownBy(() -> INSTANCE.getHook("mismatched", config))
-                .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("No micrometer configuration hook found for name 'mismatched'");
-    }
-
-}
\ No newline at end of file
diff --git a/kroxylicious/src/test/java/io/kroxylicious/proxy/micrometer/TestMicrometerConfigurationHookContributor.java b/kroxylicious/src/test/java/io/kroxylicious/proxy/micrometer/TestMicrometerConfigurationHookContributor.java
index 56c0fa82..d905f286 100644
--- a/kroxylicious/src/test/java/io/kroxylicious/proxy/micrometer/TestMicrometerConfigurationHookContributor.java
+++ b/kroxylicious/src/test/java/io/kroxylicious/proxy/micrometer/TestMicrometerConfigurationHookContributor.java
@@ -9,6 +9,7 @@ package io.kroxylicious.proxy.micrometer;
 import java.util.Objects;
 
 import io.kroxylicious.proxy.config.BaseConfig;
+import io.kroxylicious.proxy.service.ConfigurationDefinition;
 import io.kroxylicious.proxy.service.Context;
 
 public class TestMicrometerConfigurationHookContributor implements MicrometerConfigurationHookContributor {
@@ -19,6 +20,11 @@ public class TestMicrometerConfigurationHookContributor implements MicrometerCon
 
     public static final String SHORT_NAME = "test";
 
+    @Override
+    public boolean contributes(String shortName) {
+        return Objects.equals(SHORT_NAME, shortName);
+    }
+
     @Override
     public Class<? extends BaseConfig> getConfigType(String shortName) {
         if (Objects.equals(shortName, SHORT_NAME)) {
@@ -29,6 +35,16 @@ public class TestMicrometerConfigurationHookContributor implements MicrometerCon
         }
     }
 
+    @Override
+    public ConfigurationDefinition getConfigDefinition(String shortName) {
+        if (Objects.equals(shortName, SHORT_NAME)) {
+            return new ConfigurationDefinition(Config.class);
+        }
+        else {
+            return null;
+        }
+    }
+
     @Override
     public MicrometerConfigurationHook getInstance(String shortName, Context context) {
         if (!Objects.equals(shortName, SHORT_NAME)) {
@@ -36,4 +52,4 @@ public class TestMicrometerConfigurationHookContributor implements MicrometerCon
         }
         return new TestHook(shortName, context.getConfig(), context);
     }
-}
+}
\ No newline at end of file
diff --git a/kroxylicious/src/test/java/io/kroxylicious/proxy/service/ContributionManagerTest.java b/kroxylicious/src/test/java/io/kroxylicious/proxy/service/ContributionManagerTest.java
new file mode 100644
index 00000000..84c5ae76
--- /dev/null
+++ b/kroxylicious/src/test/java/io/kroxylicious/proxy/service/ContributionManagerTest.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.proxy.service;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import io.kroxylicious.proxy.config.BaseConfig;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class ContributionManagerTest {
+
+    private List<Contributor<?, ? super Context>> contributingContributors;
+
+    @BeforeEach
+    void setUp() {
+        contributingContributors = List.of(new StringContributor("one", "v1"), new StringContributor("two", "v2", StringyConfig.class),
+                new LongContributor("three", 3));
+    }
+
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Test
+    void shouldLoadServicesOfType() {
+        // Given
+        final Function<Class, Iterable> supplier = mock(Function.class);
+        when(supplier.apply(StringContributor.class)).thenReturn(List.of(new StringContributor("testType", "testValue")));
+        final ContributionManager contributionManager = new ContributionManager(supplier);
+
+        // When
+        contributionManager.getDefinition(StringContributor.class, "testType");
+
+        // Then
+        verify(supplier).apply(any());
+    }
+
+    @Test
+    void shouldLoadServicesOfMultipleType() {
+        // Given
+        final ContributionManager contributionManager = new ContributionManager(clazz -> contributingContributors);
+        final ConfigurationDefinition stringConfigDef = contributionManager.getDefinition(StringContributor.class, "two");
+
+        // When
+        final ConfigurationDefinition longConfigDef = contributionManager.getDefinition(LongContributor.class, "three");
+
+        // Then
+        assertThat(stringConfigDef).hasFieldOrProperty("configurationType").extracting("configurationType").isEqualTo(StringyConfig.class);
+        assertThat(longConfigDef).hasFieldOrProperty("configurationType").extracting("configurationType").isEqualTo(LongConfig.class);
+    }
+
+    @Test
+    void shouldThrowExceptionIfShortNameIsUnknownForConfigurationDefinition() {
+        // Given
+        final ContributionManager contributionManager = new ContributionManager(clazz -> contributingContributors);
+
+        // When
+        assertThrows(IllegalArgumentException.class, () -> contributionManager.getDefinition(StringContributor.class, "unknown"));
+
+        // Then
+    }
+
+    @Test
+    void shouldFindConfigDefinitionByShortName() {
+        // Given
+        final ContributionManager contributionManager = new ContributionManager(clazz -> contributingContributors);
+
+        // When
+        final ConfigurationDefinition configurationDefinition = contributionManager.getDefinition(StringContributor.class, "two");
+
+        // Then
+        assertThat(configurationDefinition).hasFieldOrProperty("configurationType").extracting("configurationType").isEqualTo(StringyConfig.class);
+    }
+
+    @Test
+    void shouldThrowExceptionIfShortNameIsUnknownForInstance() {
+        // Given
+        final ContributionManager contributionManager = new ContributionManager(clazz -> contributingContributors);
+
+        // When
+        assertThrows(IllegalArgumentException.class, () -> contributionManager.getInstance(StringContributor.class, "unknown", () -> null));
+
+        // Then
+    }
+
+    @Test
+    void shouldGetInstanceByShortName() {
+        // Given
+        final ContributionManager contributionManager = new ContributionManager(clazz -> contributingContributors);
+
+        // When
+        final String actualInstance = contributionManager.getInstance(StringContributor.class, "two", () -> null);
+
+        // Then
+        assertThat(actualInstance).isEqualTo("v2");
+    }
+
+    private static class LongContributor implements Contributor<Long, Context> {
+        private final String myTypeName;
+        private final long value;
+
+        private LongContributor(String typeName, long value) {
+            this.myTypeName = typeName;
+            this.value = value;
+        }
+
+        @Override
+        public boolean contributes(String typeName) {
+            return Objects.equals(this.myTypeName, typeName);
+        }
+
+        @Override
+        public Class<? extends BaseConfig> getConfigType(String shortName) {
+            return LongConfig.class;
+        }
+
+        @Override
+        public ConfigurationDefinition getConfigDefinition(String shortName) {
+            return new ConfigurationDefinition(LongConfig.class);
+        }
+
+        @Override
+        public Long getInstance(String shortName, Context context) {
+            return value;
+        }
+    }
+
+    private static class StringContributor implements Contributor<String, Context> {
+
+        private final String myTypeName;
+        private final String value;
+        private final Class<? extends BaseConfig> configurationType;
+
+        private StringContributor(String typeName, String value) {
+            this(typeName, value, StringConfig.class);
+        }
+
+        private StringContributor(String typeName, String value, Class<? extends BaseConfig> configurationType) {
+            this.myTypeName = typeName;
+            this.value = value;
+            this.configurationType = configurationType;
+        }
+
+        @Override
+        public boolean contributes(String typeName) {
+            return Objects.equals(this.myTypeName, typeName);
+        }
+
+        @Override
+        public Class<? extends BaseConfig> getConfigType(String shortName) {
+            return getConfigDefinition(shortName).configurationType();
+        }
+
+        @Override
+        public ConfigurationDefinition getConfigDefinition(String shortName) {
+            return new ConfigurationDefinition(configurationType);
+        }
+
+        @Override
+        public String getInstance(String shortName, Context context) {
+            return value;
+        }
+    }
+
+    private static class StringConfig extends BaseConfig {
+
+    }
+
+    private static class LongConfig extends BaseConfig {
+
+    }
+
+    private static class StringyConfig extends BaseConfig {
+
+    }
+}
\ No newline at end of file