Repository: kroxylicious/kroxylicious
Pull request #581 (2023-08-28 01:52:55)

Enable Filter Contributors to access netty event loop while constructing Filters - alternative
### Type of change

- Enhancement / new feature

### Description

We want to begin making some resources available to Filter instances at contruction time. Some examples might be:
* executors like the event loop for the filter, or a shared blocking pool
* user-defined expensive components which may have their own threading model and pools
* virtual cluster name
* shared state that we want to be available across channels

This adds a concept of `Context` to `io.kroxylicious.proxy.service`:
```
public interface Context {
    BaseConfig getConfig();
}
```
and the Contributor interface changes to
```
public interface Contributor<T, S extends Context> {
    Class<? extends BaseConfig> getConfigType(String shortName);

    T getInstance(String shortName, S context);
}
```

`Context` is meant to represent information about the context in which the Contributor is being invoked to get an instance. So for a FilterContributor, the invocation will be relative to a virtual cluster and a netty channel. It also contains the configuration object parsed from the YAML configuration file.

We then add a `FilterContext` class used by `FilterContributor` that can offer the netty event loop, plus extend the `BaseContributor` to provide typesafe conveniences for registering filters. Users of BaseContributor will be able

### example usage

given a filter implementation `ExampleFilter` with a custom configuration class `ExampleConfig` and a constructor like:
```
ExampleFilter(ScheduledExecutorService executorService, ExampleConfig config)
````
We can populate it with the event loop in a custom Contributor implementation like this":

```java
import io.kroxylicious.proxy.service.BaseContributor;

public class ExampleContributor extends BaseContributor<KrpcFilter, FilterContext> implements FilterContributor {

    public static final BaseContributorBuilder<KrpcFilter, FilterContext> FILTERS = BaseContributor.<KrpcFilter, FilterContext> builder()
            .add("Example", ExampleConfig.class, (context, config) -> {
                return new ExampleFilter(context.executors().eventLoop(), config);
            });

    public TestFilterContributor() {
        super(FILTERS);
    }
}
```

Or it could accept FilterContext directly like:
```
ExampleFilter(FilterContext context, ExampleConfig config)
````

```java
import io.kroxylicious.proxy.service.BaseContributor;

public class ExampleContributor extends BaseContributor<KrpcFilter, FilterContext> implements FilterContributor {

    public static final BaseContributorBuilder<KrpcFilter, FilterContext> FILTERS = BaseContributor.<KrpcFilter, FilterContext> builder()
            .add("Example", ExampleConfig.class, ExampleFilter::new);

    public TestFilterContributor() {
        super(FILTERS);
    }
}
```

Or if there is no configuration for that filter it could accept:
```
ExampleFilter(FilterContext context)
````

```java
import io.kroxylicious.proxy.service.BaseContributor;

public class ExampleContributor extends BaseContributor<KrpcFilter, FilterContext> implements FilterContributor {

    public static final BaseContributorBuilder<KrpcFilter, FilterContext> FILTERS = BaseContributor.<KrpcFilter, FilterContext> builder()
            .add("Example", ExampleFilter::new);

    public TestFilterContributor() {
        super(FILTERS);
    }
}
```

or

```
ExampleFilter(ScheduledExecutorService service)
````

```java
import io.kroxylicious.proxy.service.BaseContributor;

public class ExampleContributor extends BaseContributor<KrpcFilter, FilterContext> implements FilterContributor {

    public static final BaseContributorBuilder<KrpcFilter, FilterContext> FILTERS = BaseContributor.<KrpcFilter, FilterContext> builder()
            .add("Example", context -> new ExampleFilter(context.executors().eventLoop()));

    public TestFilterContributor() {
        super(FILTERS);
    }
}
```
### Checklist

_Please go through this checklist and make sure all applicable tasks have been done_

- [ ] Write tests
- [ ] 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).


robobario: I think this should supercede #575 as it's nicer that every Contributor's Context does something useful (contain the BaseConfig).
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=581)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&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=581&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&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=581&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=581&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&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=581&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=581&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=581&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=581&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=581&resolved=false&types=CODE_SMELL) [2 Code Smells](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&resolved=false&types=CODE_SMELL)

[![62.5%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '62.5%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=581&metric=new_coverage&view=list) [62.5% Coverage](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=581&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=581&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=581&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]: 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=581)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&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=581&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&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=581&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=581&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&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=581&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=581&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=581&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=581&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=581&resolved=false&types=CODE_SMELL) [2 Code Smells](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&resolved=false&types=CODE_SMELL)

[![62.5%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '62.5%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=581&metric=new_coverage&view=list) [62.5% Coverage](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=581&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=581&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=581&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=581)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&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=581&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&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=581&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=581&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&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=581&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=581&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=581&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=581&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=581&resolved=false&types=CODE_SMELL) [2 Code Smells](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&resolved=false&types=CODE_SMELL)

[![85.4%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '85.4%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=581&metric=new_coverage&view=list) [85.4% Coverage](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=581&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=581&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=581&metric=new_duplicated_lines_density&view=list)


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=581)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&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=581&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&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=581&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=581&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&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=581&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=581&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=581&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=581&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=581&resolved=false&types=CODE_SMELL) [2 Code Smells](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&resolved=false&types=CODE_SMELL)

[![88.6%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '88.6%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=581&metric=new_coverage&view=list) [88.6% Coverage](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=581&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=581&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=581&metric=new_duplicated_lines_density&view=list)


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=581)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&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=581&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&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=581&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=581&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&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=581&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=581&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=581&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=581&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=581&resolved=false&types=CODE_SMELL) [2 Code Smells](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&resolved=false&types=CODE_SMELL)

[![88.6%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '88.6%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=581&metric=new_coverage&view=list) [88.6% Coverage](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=581&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=581&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=581&metric=new_duplicated_lines_density&view=list)


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=581)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&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=581&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&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=581&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=581&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&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=581&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=581&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=581&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=581&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=581&resolved=false&types=CODE_SMELL) [2 Code Smells](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&resolved=false&types=CODE_SMELL)

[![88.6%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '88.6%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=581&metric=new_coverage&view=list) [88.6% Coverage](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=581&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=581&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=581&metric=new_duplicated_lines_density&view=list)


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=581)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&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=581&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&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=581&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=581&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&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=581&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=581&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=581&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=581&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=581&resolved=false&types=CODE_SMELL) [2 Code Smells](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=581&resolved=false&types=CODE_SMELL)

[![88.6%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '88.6%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=581&metric=new_coverage&view=list) [88.6% Coverage](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=581&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=581&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=581&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/kroxylicious-additional-filters/kroxylicious-multitenant/src/main/java/io/kroxylicious/proxy/filter/multitenant/MultiTenantFilterContributor.java b/kroxylicious-additional-filters/kroxylicious-multitenant/src/main/java/io/kroxylicious/proxy/filter/multitenant/MultiTenantFilterContributor.java
index f09523a9..8b0a8600 100644
--- a/kroxylicious-additional-filters/kroxylicious-multitenant/src/main/java/io/kroxylicious/proxy/filter/multitenant/MultiTenantFilterContributor.java
+++ b/kroxylicious-additional-filters/kroxylicious-multitenant/src/main/java/io/kroxylicious/proxy/filter/multitenant/MultiTenantFilterContributor.java
@@ -6,12 +6,13 @@
 package io.kroxylicious.proxy.filter.multitenant;
 
 import io.kroxylicious.proxy.filter.Filter;
+import io.kroxylicious.proxy.filter.FilterConstructContext;
 import io.kroxylicious.proxy.filter.FilterContributor;
 import io.kroxylicious.proxy.service.BaseContributor;
 
-public class MultiTenantFilterContributor extends BaseContributor<Filter> implements FilterContributor {
+public class MultiTenantFilterContributor extends BaseContributor<Filter, FilterConstructContext> implements FilterContributor {
 
-    public static final BaseContributorBuilder<Filter> FILTERS = BaseContributor.<Filter> builder()
+    public static final BaseContributorBuilder<Filter, FilterConstructContext> FILTERS = BaseContributor.<Filter, FilterConstructContext> builder()
             .add("MultiTenant", MultiTenantTransformationFilter::new);
 
     public MultiTenantFilterContributor() {
diff --git a/kroxylicious-additional-filters/kroxylicious-schema-validation/src/main/java/io/kroxylicious/proxy/filter/schema/ProduceRequestValidationFilterContributor.java b/kroxylicious-additional-filters/kroxylicious-schema-validation/src/main/java/io/kroxylicious/proxy/filter/schema/ProduceRequestValidationFilterContributor.java
index e88edd9d..ee046b64 100644
--- a/kroxylicious-additional-filters/kroxylicious-schema-validation/src/main/java/io/kroxylicious/proxy/filter/schema/ProduceRequestValidationFilterContributor.java
+++ b/kroxylicious-additional-filters/kroxylicious-schema-validation/src/main/java/io/kroxylicious/proxy/filter/schema/ProduceRequestValidationFilterContributor.java
@@ -6,6 +6,7 @@
 package io.kroxylicious.proxy.filter.schema;
 
 import io.kroxylicious.proxy.filter.Filter;
+import io.kroxylicious.proxy.filter.FilterConstructContext;
 import io.kroxylicious.proxy.filter.FilterContributor;
 import io.kroxylicious.proxy.filter.schema.config.ValidationConfig;
 import io.kroxylicious.proxy.filter.schema.validation.request.ProduceRequestValidator;
@@ -14,9 +15,9 @@ import io.kroxylicious.proxy.service.BaseContributor;
 /**
  * Contributor for request validation filters
  */
-public class ProduceRequestValidationFilterContributor extends BaseContributor<Filter> implements FilterContributor {
+public class ProduceRequestValidationFilterContributor extends BaseContributor<Filter, FilterConstructContext> implements FilterContributor {
 
-    private static final BaseContributorBuilder<Filter> FILTERS = BaseContributor.<Filter> builder()
+    private static final BaseContributorBuilder<Filter, FilterConstructContext> FILTERS = BaseContributor.<Filter, FilterConstructContext> builder()
             .add("ProduceValidator", ValidationConfig.class, (config) -> {
                 ProduceRequestValidator validator = ProduceValidationFilterBuilder.build(config);
                 return new ProduceValidationFilter(config.isForwardPartialRequests(), validator);
diff --git a/kroxylicious-api/src/main/java/io/kroxylicious/proxy/clusternetworkaddressconfigprovider/ClusterNetworkAddressConfigProviderContributor.java b/kroxylicious-api/src/main/java/io/kroxylicious/proxy/clusternetworkaddressconfigprovider/ClusterNetworkAddressConfigProviderContributor.java
index dd443b02..f70065eb 100644
--- a/kroxylicious-api/src/main/java/io/kroxylicious/proxy/clusternetworkaddressconfigprovider/ClusterNetworkAddressConfigProviderContributor.java
+++ b/kroxylicious-api/src/main/java/io/kroxylicious/proxy/clusternetworkaddressconfigprovider/ClusterNetworkAddressConfigProviderContributor.java
@@ -6,11 +6,12 @@
 package io.kroxylicious.proxy.clusternetworkaddressconfigprovider;
 
 import io.kroxylicious.proxy.service.ClusterNetworkAddressConfigProvider;
+import io.kroxylicious.proxy.service.Context;
 import io.kroxylicious.proxy.service.Contributor;
 
 /**
  * ClusterNetworkAddressConfigProviderContributor is a pluggable source of network address information.
  * @see Contributor
  */
-public interface ClusterNetworkAddressConfigProviderContributor extends Contributor<ClusterNetworkAddressConfigProvider> {
+public interface ClusterNetworkAddressConfigProviderContributor extends Contributor<ClusterNetworkAddressConfigProvider, Context> {
 }
diff --git a/kroxylicious-api/src/main/java/io/kroxylicious/proxy/filter/FilterConstructContext.java b/kroxylicious-api/src/main/java/io/kroxylicious/proxy/filter/FilterConstructContext.java
new file mode 100644
index 00000000..e28b8be2
--- /dev/null
+++ b/kroxylicious-api/src/main/java/io/kroxylicious/proxy/filter/FilterConstructContext.java
@@ -0,0 +1,28 @@
+/*
+ * 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.filter;
+
+import io.kroxylicious.proxy.config.BaseConfig;
+import io.kroxylicious.proxy.service.Context;
+
+public interface FilterConstructContext extends Context {
+    FilterExecutors executors();
+
+    static FilterConstructContext wrap(BaseConfig config, FilterExecutors executors) {
+        return new FilterConstructContext() {
+            @Override
+            public FilterExecutors executors() {
+                return executors;
+            }
+
+            @Override
+            public BaseConfig getConfig() {
+                return config;
+            }
+        };
+    }
+}
diff --git a/kroxylicious-api/src/main/java/io/kroxylicious/proxy/filter/FilterContributor.java b/kroxylicious-api/src/main/java/io/kroxylicious/proxy/filter/FilterContributor.java
index 2dd5e299..5ae01167 100644
--- a/kroxylicious-api/src/main/java/io/kroxylicious/proxy/filter/FilterContributor.java
+++ b/kroxylicious-api/src/main/java/io/kroxylicious/proxy/filter/FilterContributor.java
@@ -11,5 +11,5 @@ import io.kroxylicious.proxy.service.Contributor;
  * FilterContributor is a pluggable source of Kroxylicious filter implementations.
  * @see Contributor
  */
-public interface FilterContributor extends Contributor<Filter> {
+public interface FilterContributor extends Contributor<Filter, FilterConstructContext> {
 }
diff --git a/kroxylicious-api/src/main/java/io/kroxylicious/proxy/filter/FilterExecutors.java b/kroxylicious-api/src/main/java/io/kroxylicious/proxy/filter/FilterExecutors.java
new file mode 100644
index 00000000..4ff92e6d
--- /dev/null
+++ b/kroxylicious-api/src/main/java/io/kroxylicious/proxy/filter/FilterExecutors.java
@@ -0,0 +1,14 @@
+/*
+ * 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.filter;
+
+import java.util.concurrent.ScheduledExecutorService;
+
+public interface FilterExecutors {
+
+    ScheduledExecutorService eventLoop();
+}
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 7e975273..3cceff4c 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
@@ -7,6 +7,7 @@ package io.kroxylicious.proxy.service;
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.function.BiFunction;
 import java.util.function.Function;
 import java.util.function.Supplier;
 
@@ -17,52 +18,60 @@ import io.kroxylicious.proxy.config.BaseConfig;
  *
  * @param <T> the service type
  */
-public abstract class BaseContributor<T> implements Contributor<T> {
+public abstract class BaseContributor<T, S extends Context> implements Contributor<T, S> {
 
-    private final Map<String, InstanceBuilder<? extends BaseConfig, T>> shortNameToInstanceBuilder;
+    private final Map<String, InstanceBuilder<T, S>> shortNameToInstanceBuilder;
 
     /**
      * Constructs and configures the contributor using the supplied {@code builder}.
      * @param builder builder
      */
-    public BaseContributor(BaseContributorBuilder<T> builder) {
+    protected BaseContributor(BaseContributorBuilder<T, S> builder) {
         shortNameToInstanceBuilder = builder.build();
     }
 
     @Override
     public Class<? extends BaseConfig> getConfigType(String shortName) {
-        InstanceBuilder<?, T> instanceBuilder = shortNameToInstanceBuilder.get(shortName);
+        InstanceBuilder<T, S> instanceBuilder = shortNameToInstanceBuilder.get(shortName);
         return instanceBuilder == null ? null : instanceBuilder.configClass;
     }
 
     @Override
-    public T getInstance(String shortName, BaseConfig config) {
-        InstanceBuilder<? extends BaseConfig, T> instanceBuilder = shortNameToInstanceBuilder.get(shortName);
-        return instanceBuilder == null ? null : instanceBuilder.construct(config);
+    public T getInstance(String shortName, S context) {
+        InstanceBuilder<T, S> instanceBuilder = shortNameToInstanceBuilder.get(shortName);
+        return instanceBuilder == null ? null : instanceBuilder.construct(context);
     }
 
-    private static class InstanceBuilder<T extends BaseConfig, L> {
+    private static class InstanceBuilder<L, D extends Context> {
 
-        private final Class<T> configClass;
-        private final Function<T, L> instanceFunction;
+        private final Class<? extends BaseConfig> configClass;
+        private final Function<D, L> instanceFunction;
 
-        InstanceBuilder(Class<T> configClass, Function<T, L> instanceFunction) {
+        InstanceBuilder(Class<? extends BaseConfig> configClass, Function<D, L> instanceFunction) {
             this.configClass = configClass;
             this.instanceFunction = instanceFunction;
         }
 
-        L construct(BaseConfig config) {
-            if (config == null) {
-                // tests pass in a null config, which some instance functions can tolerate
-                return instanceFunction.apply(null);
-            }
-            else if (configClass.isAssignableFrom(config.getClass())) {
-                return instanceFunction.apply(configClass.cast(config));
-            }
-            else {
-                throw new IllegalArgumentException("config has the wrong type, expected "
-                        + configClass.getName() + ", got " + config.getClass().getName());
-            }
+        L construct(D context) {
+            return instanceFunction.apply(context);
+        }
+
+        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 -> {
+                BaseConfig config = context.getConfig();
+                if (config == null) {
+                    // tests pass in a null config, which some instance functions can tolerate
+                    return instanceFunction.apply(context, null);
+                }
+                else if (configClass.isAssignableFrom(config.getClass())) {
+                    return instanceFunction.apply(context, configClass.cast(config));
+                }
+                else {
+                    throw new IllegalArgumentException("config has the wrong type, expected "
+                            + configClass.getName() + ", got " + config.getClass().getName());
+                }
+            });
+
         }
     }
 
@@ -71,12 +80,12 @@ public abstract class BaseContributor<T> implements Contributor<T> {
      * @see BaseContributor#builder()
      * @param <L> the service type
      */
-    public static class BaseContributorBuilder<L> {
+    public static class BaseContributorBuilder<L, D extends Context> {
 
         private BaseContributorBuilder() {
         }
 
-        private final Map<String, InstanceBuilder<?, L>> shortNameToInstanceBuilder = new HashMap<>();
+        private final Map<String, InstanceBuilder<L, D>> shortNameToInstanceBuilder = new HashMap<>();
 
         /**
          * Registers a factory function for the construction of a service instance.
@@ -87,11 +96,24 @@ public abstract class BaseContributor<T> implements Contributor<T> {
          * @return this
          * @param <T> the configuration concrete type
          */
-        public <T extends BaseConfig> BaseContributorBuilder<L> add(String shortName, Class<T> configClass, Function<T, L> instanceFunction) {
+        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));
+        }
+
+        /**
+         * Registers a factory function for the construction of a service instance.
+         *
+         * @param shortName 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");
             }
-            shortNameToInstanceBuilder.put(shortName, new InstanceBuilder<>(configClass, instanceFunction));
+            shortNameToInstanceBuilder.put(shortName, InstanceBuilder.builder(configClass, instanceFunction));
             return this;
         }
 
@@ -102,11 +124,22 @@ public abstract class BaseContributor<T> implements Contributor<T> {
          * @param instanceFunction function that constructs the service instance
          * @return this
          */
-        public BaseContributorBuilder<L> add(String shortName, Supplier<L> instanceFunction) {
-            return add(shortName, BaseConfig.class, (config) -> instanceFunction.get());
+        public BaseContributorBuilder<L, D> add(String shortName, Supplier<L> instanceFunction) {
+            return add(shortName, BaseConfig.class, config -> instanceFunction.get());
+        }
+
+        /**
+         * Registers a factory function for the construction of a service instance.
+         *
+         * @param shortName 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));
         }
 
-        Map<String, InstanceBuilder<?, L>> build() {
+        Map<String, InstanceBuilder<L, D>> build() {
             return Map.copyOf(shortNameToInstanceBuilder);
         }
     }
@@ -117,7 +150,7 @@ public abstract class BaseContributor<T> implements Contributor<T> {
      * @return the builder
      * @param <L> the service type
      */
-    public static <L> BaseContributorBuilder<L> builder() {
+    public static <L, D extends Context> BaseContributorBuilder<L, D> builder() {
         return new BaseContributorBuilder<>();
     }
 }
diff --git a/kroxylicious-api/src/main/java/io/kroxylicious/proxy/service/Context.java b/kroxylicious-api/src/main/java/io/kroxylicious/proxy/service/Context.java
new file mode 100644
index 00000000..9fc1120e
--- /dev/null
+++ b/kroxylicious-api/src/main/java/io/kroxylicious/proxy/service/Context.java
@@ -0,0 +1,26 @@
+/*
+ * 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;
+
+/**
+ * Context in which a Contributor is getting an instance, this includes the user-supplied configuration.
+ */
+public interface Context {
+
+    /**
+     * service configuration which may be null if the service instance does not accept configuration.
+     * @return config
+     */
+    BaseConfig getConfig();
+
+    static Context wrap(BaseConfig config) {
+        return () -> config;
+    }
+
+}
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 5c32a049..d5913d63 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
@@ -13,7 +13,7 @@ import io.kroxylicious.proxy.config.BaseConfig;
  *
  * @param <T> the service type
  */
-public interface Contributor<T> {
+public interface Contributor<T, S extends Context> {
 
     /**
      * Gets the concrete type of the configuration required by this service instance.
@@ -27,8 +27,8 @@ public interface Contributor<T> {
      * Creates an instance of the service.
      *
      * @param shortName service short name
-     * @param config    service configuration which may be null if the service instance does not accept configuration.
+     * @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, BaseConfig config);
+    T getInstance(String shortName, S context);
 }
diff --git a/kroxylicious-sample/src/main/java/io/kroxylicious/sample/SampleContributor.java b/kroxylicious-sample/src/main/java/io/kroxylicious/sample/SampleContributor.java
index e094a85e..1f1a2c6e 100644
--- a/kroxylicious-sample/src/main/java/io/kroxylicious/sample/SampleContributor.java
+++ b/kroxylicious-sample/src/main/java/io/kroxylicious/sample/SampleContributor.java
@@ -7,15 +7,16 @@
 package io.kroxylicious.sample;
 
 import io.kroxylicious.proxy.filter.Filter;
+import io.kroxylicious.proxy.filter.FilterConstructContext;
 import io.kroxylicious.proxy.filter.FilterContributor;
 import io.kroxylicious.proxy.service.BaseContributor;
 import io.kroxylicious.sample.config.SampleFilterConfig;
 
-public class SampleContributor extends BaseContributor<Filter> implements FilterContributor {
+public class SampleContributor extends BaseContributor<Filter, FilterConstructContext> implements FilterContributor {
 
     public static final String SAMPLE_FETCH = "SampleFetchResponse";
     public static final String SAMPLE_PRODUCE = "SampleProduceRequest";
-    public static final BaseContributorBuilder<Filter> FILTERS = BaseContributor.<Filter> builder()
+    public static final BaseContributorBuilder<Filter, FilterConstructContext> FILTERS = BaseContributor.<Filter, FilterConstructContext> builder()
             .add(SAMPLE_FETCH, SampleFilterConfig.class, SampleFetchResponseFilter::new)
             .add(SAMPLE_PRODUCE, SampleFilterConfig.class, SampleProduceRequestFilter::new);
 
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 114de615..c36206f1 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/bootstrap/FilterChainFactory.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/bootstrap/FilterChainFactory.java
@@ -6,11 +6,13 @@
 package io.kroxylicious.proxy.bootstrap;
 
 import java.util.List;
+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.internal.filter.NettyFilterContext;
 
 /**
  * Abstracts the creation of a chain of filter instances, hiding the configuration
@@ -22,6 +24,7 @@ public class FilterChainFactory {
     private final Configuration config;
 
     public FilterChainFactory(Configuration config) {
+        Objects.requireNonNull(config);
         this.config = config;
     }
 
@@ -30,7 +33,7 @@ public class FilterChainFactory {
      *
      * @return the new chain.
      */
-    public List<FilterAndInvoker> createFilters() {
+    public List<FilterAndInvoker> createFilters(NettyFilterContext context) {
         FilterContributorManager filterContributorManager = FilterContributorManager.getInstance();
 
         List<FilterDefinition> filters = config.filters();
@@ -39,7 +42,7 @@ public class FilterChainFactory {
         }
         return filters
                 .stream()
-                .map(f -> filterContributorManager.getFilter(f.type(), f.config()))
+                .map(f -> filterContributorManager.getFilter(f.type(), context, f.config()))
                 .flatMap(filter -> FilterAndInvoker.build(filter).stream())
                 .toList();
     }
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/KafkaProxyInitializer.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/KafkaProxyInitializer.java
index f90ed9bb..5e2d2864 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/KafkaProxyInitializer.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/KafkaProxyInitializer.java
@@ -34,6 +34,7 @@ import io.kroxylicious.proxy.internal.codec.KafkaResponseEncoder;
 import io.kroxylicious.proxy.internal.filter.ApiVersionsIntersectFilter;
 import io.kroxylicious.proxy.internal.filter.BrokerAddressFilter;
 import io.kroxylicious.proxy.internal.filter.EagerMetadataLearner;
+import io.kroxylicious.proxy.internal.filter.NettyFilterContext;
 import io.kroxylicious.proxy.internal.net.Endpoint;
 import io.kroxylicious.proxy.internal.net.EndpointReconciler;
 import io.kroxylicious.proxy.internal.net.VirtualClusterBinding;
@@ -175,7 +176,7 @@ public class KafkaProxyInitializer extends ChannelInitializer<SocketChannel> {
             var filterChainFactory = new FilterChainFactory(config);
             List<FilterAndInvoker> apiVersionFilters = dp.isAuthenticationOffloadEnabled() ? List.of()
                     : FilterAndInvoker.build(new ApiVersionsIntersectFilter(apiVersionService));
-            List<FilterAndInvoker> customProtocolFilters = filterChainFactory.createFilters();
+            List<FilterAndInvoker> customProtocolFilters = filterChainFactory.createFilters(new NettyFilterContext(ch.eventLoop()));
             List<FilterAndInvoker> brokerAddressFilters = FilterAndInvoker.build(new BrokerAddressFilter(virtualCluster, endpointReconciler));
             var filters = new ArrayList<>(apiVersionFilters);
             filters.addAll(customProtocolFilters);
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/BuiltinClusterNetworkAddressConfigProviderContributor.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/BuiltinClusterNetworkAddressConfigProviderContributor.java
index b06cefb7..fa6f92b2 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/BuiltinClusterNetworkAddressConfigProviderContributor.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/BuiltinClusterNetworkAddressConfigProviderContributor.java
@@ -10,11 +10,13 @@ import io.kroxylicious.proxy.internal.clusternetworkaddressconfigprovider.PortPe
 import io.kroxylicious.proxy.internal.clusternetworkaddressconfigprovider.SniRoutingClusterNetworkAddressConfigProvider.SniRoutingClusterNetworkAddressConfigProviderConfig;
 import io.kroxylicious.proxy.service.BaseContributor;
 import io.kroxylicious.proxy.service.ClusterNetworkAddressConfigProvider;
+import io.kroxylicious.proxy.service.Context;
 
-public class BuiltinClusterNetworkAddressConfigProviderContributor extends BaseContributor<ClusterNetworkAddressConfigProvider>
+public class BuiltinClusterNetworkAddressConfigProviderContributor extends BaseContributor<ClusterNetworkAddressConfigProvider, Context>
         implements ClusterNetworkAddressConfigProviderContributor {
 
-    public static final BaseContributorBuilder<ClusterNetworkAddressConfigProvider> FILTERS = BaseContributor.<ClusterNetworkAddressConfigProvider> builder()
+    public static final BaseContributorBuilder<ClusterNetworkAddressConfigProvider, Context> FILTERS = BaseContributor
+            .<ClusterNetworkAddressConfigProvider, Context> builder()
             .add("PortPerBroker", PortPerBrokerClusterNetworkAddressConfigProviderConfig.class, PortPerBrokerClusterNetworkAddressConfigProvider::new)
             .add("SniRouting", SniRoutingClusterNetworkAddressConfigProviderConfig.class, SniRoutingClusterNetworkAddressConfigProvider::new);
 
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
index 745ac09b..0d4b4a77 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/ClusterNetworkAddressConfigProviderContributorManager.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/ClusterNetworkAddressConfigProviderContributorManager.java
@@ -12,6 +12,8 @@ import io.kroxylicious.proxy.clusternetworkaddressconfigprovider.ClusterNetworkA
 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();
@@ -41,7 +43,7 @@ public class ClusterNetworkAddressConfigProviderContributorManager {
 
     public ClusterNetworkAddressConfigProvider getClusterEndpointConfigProvider(String shortName, BaseConfig baseConfig) {
         for (ClusterNetworkAddressConfigProviderContributor contributor : contributors) {
-            var assigner = contributor.getInstance(shortName, baseConfig);
+            var assigner = contributor.getInstance(shortName, wrap(baseConfig));
             if (assigner != null) {
                 return assigner;
             }
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/filter/BuiltinFilterContributor.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/filter/BuiltinFilterContributor.java
index 63565b95..e629c6ac 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/filter/BuiltinFilterContributor.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/filter/BuiltinFilterContributor.java
@@ -6,14 +6,15 @@
 package io.kroxylicious.proxy.internal.filter;
 
 import io.kroxylicious.proxy.filter.Filter;
+import io.kroxylicious.proxy.filter.FilterConstructContext;
 import io.kroxylicious.proxy.filter.FilterContributor;
 import io.kroxylicious.proxy.internal.filter.FetchResponseTransformationFilter.FetchResponseTransformationConfig;
 import io.kroxylicious.proxy.internal.filter.ProduceRequestTransformationFilter.ProduceRequestTransformationConfig;
 import io.kroxylicious.proxy.service.BaseContributor;
 
-public class BuiltinFilterContributor extends BaseContributor<Filter> implements FilterContributor {
+public class BuiltinFilterContributor extends BaseContributor<Filter, FilterConstructContext> implements FilterContributor {
 
-    public static final BaseContributorBuilder<Filter> FILTERS = BaseContributor.<Filter> builder()
+    public static final BaseContributorBuilder<Filter, FilterConstructContext> FILTERS = BaseContributor.<Filter, FilterConstructContext> builder()
             .add("ProduceRequestTransformation", ProduceRequestTransformationConfig.class, ProduceRequestTransformationFilter::new)
             .add("FetchResponseTransformation", FetchResponseTransformationConfig.class, FetchResponseTransformationFilter::new);
 
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
index b41ccf84..f53477fb 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/filter/FilterContributorManager.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/filter/FilterContributorManager.java
@@ -10,6 +10,7 @@ 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 {
@@ -39,11 +40,12 @@ public class FilterContributorManager {
         throw new IllegalArgumentException("No filter found for name '" + shortName + "'");
     }
 
-    public Filter getFilter(String shortName, BaseConfig filterConfig) {
+    public Filter getFilter(String shortName, NettyFilterContext context, BaseConfig filterConfig) {
         Iterator<FilterContributor> it = contributors.iterator();
         while (it.hasNext()) {
             FilterContributor contributor = it.next();
-            Filter filter = contributor.getInstance(shortName, filterConfig);
+            FilterConstructContext context1 = context.wrap(filterConfig);
+            Filter filter = contributor.getInstance(shortName, context1);
             if (filter != null) {
                 return filter;
             }
@@ -51,4 +53,5 @@ public class FilterContributorManager {
 
         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
new file mode 100644
index 00000000..e24f5f44
--- /dev/null
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/filter/NettyFilterContext.java
@@ -0,0 +1,43 @@
+/*
+ * 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.concurrent.ScheduledExecutorService;
+
+import io.kroxylicious.proxy.config.BaseConfig;
+import io.kroxylicious.proxy.filter.FilterConstructContext;
+import io.kroxylicious.proxy.filter.FilterExecutors;
+
+public class NettyFilterContext {
+    private final ScheduledExecutorService eventLoop;
+
+    public NettyFilterContext(ScheduledExecutorService eventLoop) {
+        this.eventLoop = eventLoop;
+    }
+
+    FilterConstructContext wrap(BaseConfig config) {
+        return new NettyFilterConfigContext(config);
+    }
+
+    class NettyFilterConfigContext implements FilterConstructContext {
+        private final BaseConfig filterConfig;
+
+        NettyFilterConfigContext(BaseConfig filterConfig) {
+            this.filterConfig = filterConfig;
+        }
+
+        @Override
+        public FilterExecutors executors() {
+            return () -> eventLoop;
+        }
+
+        @Override
+        public BaseConfig getConfig() {
+            return filterConfig;
+        }
+    }
+}
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/micrometer/DefaultMicrometerConfigurationHookContributor.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/micrometer/DefaultMicrometerConfigurationHookContributor.java
index f2c22da2..5e0f0c68 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/micrometer/DefaultMicrometerConfigurationHookContributor.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/micrometer/DefaultMicrometerConfigurationHookContributor.java
@@ -6,10 +6,12 @@
 package io.kroxylicious.proxy.micrometer;
 
 import io.kroxylicious.proxy.service.BaseContributor;
+import io.kroxylicious.proxy.service.Context;
 
-public class DefaultMicrometerConfigurationHookContributor extends BaseContributor<MicrometerConfigurationHook> implements MicrometerConfigurationHookContributor {
+public class DefaultMicrometerConfigurationHookContributor extends BaseContributor<MicrometerConfigurationHook, Context>
+        implements MicrometerConfigurationHookContributor {
 
-    public static final BaseContributorBuilder<MicrometerConfigurationHook> BUILDER = BaseContributor.<MicrometerConfigurationHook> builder()
+    public static final BaseContributorBuilder<MicrometerConfigurationHook, Context> BUILDER = BaseContributor.<MicrometerConfigurationHook, Context> builder()
             .add("CommonTags", CommonTagsHook.CommonTagsHookConfig.class, CommonTagsHook::new)
             .add("StandardBinders", StandardBindersHook.StandardBindersHookConfig.class, StandardBindersHook::new);
 
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/micrometer/MicrometerConfigurationHookContributor.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/micrometer/MicrometerConfigurationHookContributor.java
index 16556a63..50507e73 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/micrometer/MicrometerConfigurationHookContributor.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/micrometer/MicrometerConfigurationHookContributor.java
@@ -5,7 +5,8 @@
  */
 package io.kroxylicious.proxy.micrometer;
 
+import io.kroxylicious.proxy.service.Context;
 import io.kroxylicious.proxy.service.Contributor;
 
-public interface MicrometerConfigurationHookContributor extends Contributor<MicrometerConfigurationHook> {
+public interface MicrometerConfigurationHookContributor extends Contributor<MicrometerConfigurationHook, Context> {
 }
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/micrometer/MicrometerConfigurationHookContributorManager.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/micrometer/MicrometerConfigurationHookContributorManager.java
index 30faa754..f0d081e2 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/micrometer/MicrometerConfigurationHookContributorManager.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/micrometer/MicrometerConfigurationHookContributorManager.java
@@ -9,6 +9,8 @@ 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();
@@ -36,7 +38,7 @@ public class MicrometerConfigurationHookContributorManager {
 
     public MicrometerConfigurationHook getHook(String shortName, BaseConfig filterConfig) {
         for (MicrometerConfigurationHookContributor contributor : contributors) {
-            MicrometerConfigurationHook hook = contributor.getInstance(shortName, filterConfig);
+            MicrometerConfigurationHook hook = contributor.getInstance(shortName, wrap(filterConfig));
             if (hook != null) {
                 return hook;
             }


Test patch:
diff --git a/integrationtests/src/test/java/io/kroxylicious/proxy/FilterIT.java b/integrationtests/src/test/java/io/kroxylicious/proxy/FilterIT.java
index fd860042..cc0844bc 100644
--- a/integrationtests/src/test/java/io/kroxylicious/proxy/FilterIT.java
+++ b/integrationtests/src/test/java/io/kroxylicious/proxy/FilterIT.java
@@ -247,6 +247,19 @@ class FilterIT {
                 ForwardingStyle.ASYNCHRONOUS_DELAYED);
     }
 
+    /**
+     * This test verifies the use-case where a filter needs delay a request/response forward
+     * until a 3rd party asynchronous action completes.
+     * @param direction direction of the flow
+     */
+    @ParameterizedTest
+    @EnumSource(value = RequestResponseMarkingFilter.Direction.class)
+    void supportsForwardDeferredByAsynchronousActionOnEventLoop(RequestResponseMarkingFilter.Direction direction) {
+        doSupportsForwardDeferredByAsynchronousRequest(direction,
+                "supportsForwardDeferredByAsynchronousActionOnEventLoop",
+                ForwardingStyle.ASYNCHRONOUS_DELAYED_ON_EVENTlOOP);
+    }
+
     /**
      * This test verifies the use-case where a filter needs delay a request/response forward
      * until an asynchronous request to the broker completes.
diff --git a/integrationtests/src/test/java/io/kroxylicious/proxy/filter/ForwardingContext.java b/integrationtests/src/test/java/io/kroxylicious/proxy/filter/ForwardingContext.java
new file mode 100644
index 00000000..5370df87
--- /dev/null
+++ b/integrationtests/src/test/java/io/kroxylicious/proxy/filter/ForwardingContext.java
@@ -0,0 +1,11 @@
+/*
+ * 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.filter;
+
+import org.apache.kafka.common.protocol.ApiMessage;
+
+public record ForwardingContext(FilterContext filterContext, FilterConstructContext constructionContext, ApiMessage body) {}
diff --git a/integrationtests/src/test/java/io/kroxylicious/proxy/filter/ForwardingStyle.java b/integrationtests/src/test/java/io/kroxylicious/proxy/filter/ForwardingStyle.java
index 10a5df12..3d9e0d05 100644
--- a/integrationtests/src/test/java/io/kroxylicious/proxy/filter/ForwardingStyle.java
+++ b/integrationtests/src/test/java/io/kroxylicious/proxy/filter/ForwardingStyle.java
@@ -11,7 +11,7 @@ import java.util.concurrent.CompletionStage;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
-import java.util.function.BiFunction;
+import java.util.function.Function;
 
 import org.apache.kafka.common.message.ListGroupsRequestData;
 import org.apache.kafka.common.message.ListGroupsResponseData;
@@ -19,21 +19,21 @@ import org.apache.kafka.common.protocol.ApiKeys;
 import org.apache.kafka.common.protocol.ApiMessage;
 import org.apache.kafka.common.protocol.Errors;
 
-public enum ForwardingStyle implements BiFunction<FilterContext, ApiMessage, CompletionStage<ApiMessage>> {
+public enum ForwardingStyle implements Function<ForwardingContext, CompletionStage<ApiMessage>> {
     SYNCHRONOUS {
         @Override
-        public CompletionStage<ApiMessage> apply(FilterContext context, ApiMessage body) {
-            return CompletableFuture.completedStage(body);
+        public CompletionStage<ApiMessage> apply(ForwardingContext context) {
+            return CompletableFuture.completedStage(context.body());
         }
     },
     ASYNCHRONOUS_DELAYED {
         @Override
-        public CompletionStage<ApiMessage> apply(FilterContext context, ApiMessage body) {
+        public CompletionStage<ApiMessage> apply(ForwardingContext context) {
             CompletableFuture<ApiMessage> result = new CompletableFuture<>();
             ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
             try {
                 executor.schedule(() -> {
-                    result.complete(body);
+                    result.complete(context.body());
                 }, 200L, TimeUnit.MILLISECONDS);
             }
             finally {
@@ -42,10 +42,21 @@ public enum ForwardingStyle implements BiFunction<FilterContext, ApiMessage, Com
             return result;
         }
     },
+    ASYNCHRONOUS_DELAYED_ON_EVENTlOOP {
+        @Override
+        public CompletionStage<ApiMessage> apply(ForwardingContext context) {
+            ScheduledExecutorService executor = context.constructionContext().executors().eventLoop();
+            CompletableFuture<ApiMessage> result = new CompletableFuture<>();
+            executor.schedule(() -> {
+                result.complete(context.body());
+            }, 200L, TimeUnit.MILLISECONDS);
+            return result;
+        }
+    },
     ASYNCHRONOUS_REQUEST_TO_BROKER {
         @Override
-        public CompletionStage<ApiMessage> apply(FilterContext context, ApiMessage body) {
-            return sendAsyncRequestAndCheckForResponseErrors(context).thenApply(unused -> body);
+        public CompletionStage<ApiMessage> apply(ForwardingContext context) {
+            return sendAsyncRequestAndCheckForResponseErrors(context.filterContext()).thenApply(unused -> context.body());
         }
 
         private CompletionStage<ListGroupsResponseData> sendAsyncRequestAndCheckForResponseErrors(FilterContext filterContext) {
diff --git a/integrationtests/src/test/java/io/kroxylicious/proxy/filter/RejectingCreateTopicFilter.java b/integrationtests/src/test/java/io/kroxylicious/proxy/filter/RejectingCreateTopicFilter.java
index 1722cf2a..f638baa7 100644
--- a/integrationtests/src/test/java/io/kroxylicious/proxy/filter/RejectingCreateTopicFilter.java
+++ b/integrationtests/src/test/java/io/kroxylicious/proxy/filter/RejectingCreateTopicFilter.java
@@ -27,8 +27,10 @@ public class RejectingCreateTopicFilter implements CreateTopicsRequestFilter {
     public static final String ERROR_MESSAGE = "rejecting all topics";
     private final ForwardingStyle forwardingStyle;
     private final boolean withCloseConnection;
+    private final FilterConstructContext constructionContext;
 
-    public RejectingCreateTopicFilter(RejectingCreateTopicFilterConfig config) {
+    public RejectingCreateTopicFilter(FilterConstructContext constructionContext, RejectingCreateTopicFilterConfig config) {
+        this.constructionContext = constructionContext;
         config = config == null ? new RejectingCreateTopicFilterConfig(false, ForwardingStyle.SYNCHRONOUS) : config;
         this.withCloseConnection = config.withCloseConnection;
         this.forwardingStyle = config.forwardingStyle;
@@ -37,7 +39,7 @@ public class RejectingCreateTopicFilter implements CreateTopicsRequestFilter {
     @Override
     public CompletionStage<RequestFilterResult> onCreateTopicsRequest(short apiVersion, RequestHeaderData header, CreateTopicsRequestData request,
                                                                       FilterContext context) {
-        return forwardingStyle.apply(context, request)
+        return forwardingStyle.apply(new ForwardingContext(context, constructionContext, request))
                 .thenCompose((u) -> {
                     CreateTopicsResponseData response = new CreateTopicsResponseData();
                     CreateTopicsResponseData.CreatableTopicResultCollection topics = new CreateTopicsResponseData.CreatableTopicResultCollection();
diff --git a/integrationtests/src/test/java/io/kroxylicious/proxy/filter/RequestResponseMarkingFilter.java b/integrationtests/src/test/java/io/kroxylicious/proxy/filter/RequestResponseMarkingFilter.java
index b7947ba4..f937a61b 100644
--- a/integrationtests/src/test/java/io/kroxylicious/proxy/filter/RequestResponseMarkingFilter.java
+++ b/integrationtests/src/test/java/io/kroxylicious/proxy/filter/RequestResponseMarkingFilter.java
@@ -41,12 +41,14 @@ public class RequestResponseMarkingFilter implements RequestFilter, ResponseFilt
     }
 
     public static final int FILTER_NAME_TAG = 500;
+    private final FilterConstructContext constructionContext;
     private final String name;
     private final Set<ApiKeys> keysToMark;
     private final Set<Direction> direction;
     private final ForwardingStyle forwardingStyle;
 
-    public RequestResponseMarkingFilter(RequestResponseMarkingFilterConfig config) {
+    public RequestResponseMarkingFilter(FilterConstructContext constructionContext, RequestResponseMarkingFilterConfig config) {
+        this.constructionContext = constructionContext;
         name = config.name;
         keysToMark = config.keysToMark;
         direction = config.direction;
@@ -58,7 +60,8 @@ public class RequestResponseMarkingFilter implements RequestFilter, ResponseFilt
         if (!(direction.contains(Direction.REQUEST) && keysToMark.contains(apiKey))) {
             return context.forwardRequest(header, body);
         }
-        return forwardingStyle.apply(context, body)
+
+        return forwardingStyle.apply(new ForwardingContext(context, constructionContext, body))
                 .thenApply(request -> applyTaggedField(request, Direction.REQUEST, name))
                 .thenCompose(taggedRequest -> context.forwardRequest(header, taggedRequest));
     }
@@ -69,7 +72,7 @@ public class RequestResponseMarkingFilter implements RequestFilter, ResponseFilt
             return context.forwardResponse(header, response);
         }
 
-        return forwardingStyle.apply(context, response)
+        return forwardingStyle.apply(new ForwardingContext(context, constructionContext, response))
                 .thenApply(request -> applyTaggedField(request, Direction.RESPONSE, name))
                 .thenCompose(taggedRequest -> context.forwardResponse(header, taggedRequest));
     }
diff --git a/integrationtests/src/test/java/io/kroxylicious/proxy/filter/TestFilterContributor.java b/integrationtests/src/test/java/io/kroxylicious/proxy/filter/TestFilterContributor.java
index 8990e343..f141ac78 100644
--- a/integrationtests/src/test/java/io/kroxylicious/proxy/filter/TestFilterContributor.java
+++ b/integrationtests/src/test/java/io/kroxylicious/proxy/filter/TestFilterContributor.java
@@ -8,9 +8,9 @@ package io.kroxylicious.proxy.filter;
 import io.kroxylicious.proxy.filter.CompositePrefixingFixedClientIdFilter.CompositePrefixingFixedClientIdFilterConfig;
 import io.kroxylicious.proxy.service.BaseContributor;
 
-public class TestFilterContributor extends BaseContributor<Filter> implements FilterContributor {
+public class TestFilterContributor extends BaseContributor<Filter, FilterConstructContext> implements FilterContributor {
 
-    public static final BaseContributorBuilder<Filter> FILTERS = BaseContributor.<Filter> builder()
+    public static final BaseContributorBuilder<Filter, FilterConstructContext> FILTERS = BaseContributor.<Filter, FilterConstructContext> builder()
             .add("FixedClientId", FixedClientIdFilter.FixedClientIdFilterConfig.class, FixedClientIdFilter::new)
             .add("ApiVersionsMarkingFilter", ApiVersionsMarkingFilter::new)
             .add("RequestResponseMarking", RequestResponseMarkingFilter.RequestResponseMarkingFilterConfig.class, RequestResponseMarkingFilter::new)
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
new file mode 100644
index 00000000..ab48ca46
--- /dev/null
+++ b/kroxylicious-additional-filters/kroxylicious-multitenant/src/test/java/io/kroxylicious/proxy/filter/multitenant/MultiTenantFilterContributorTest.java
@@ -0,0 +1,34 @@
+/*
+ * 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.filter.multitenant;
+
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import io.kroxylicious.proxy.config.BaseConfig;
+import io.kroxylicious.proxy.filter.Filter;
+import io.kroxylicious.proxy.filter.FilterConstructContext;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class MultiTenantFilterContributorTest {
+
+    @Test
+    void testGetConfigType() {
+        MultiTenantFilterContributor contributor = new MultiTenantFilterContributor();
+        Class<? extends BaseConfig> configType = contributor.getConfigType("MultiTenant");
+        assertThat(configType).isEqualTo(BaseConfig.class);
+    }
+
+    @Test
+    void testGetInstance() {
+        MultiTenantFilterContributor contributor = new MultiTenantFilterContributor();
+        Filter filter = contributor.getInstance("MultiTenant", Mockito.mock(FilterConstructContext.class));
+        assertThat(filter).isNotNull().isInstanceOf(MultiTenantTransformationFilter.class);
+    }
+
+}
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
new file mode 100644
index 00000000..166c8c74
--- /dev/null
+++ b/kroxylicious-additional-filters/kroxylicious-schema-validation/src/test/java/io/kroxylicious/proxy/filter/schema/ProduceRequestValidationFilterContributorTest.java
@@ -0,0 +1,39 @@
+/*
+ * 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.filter.schema;
+
+import java.util.List;
+import java.util.concurrent.Executors;
+
+import org.junit.jupiter.api.Test;
+
+import io.kroxylicious.proxy.config.BaseConfig;
+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 static org.assertj.core.api.Assertions.assertThat;
+
+class ProduceRequestValidationFilterContributorTest {
+
+    @Test
+    void testGetConfigType() {
+        ProduceRequestValidationFilterContributor contributor = new ProduceRequestValidationFilterContributor();
+        Class<? extends BaseConfig> configType = contributor.getConfigType("ProduceValidator");
+        assertThat(configType).isEqualTo(ValidationConfig.class);
+    }
+
+    @Test
+    void testGetInstance() {
+        ProduceRequestValidationFilterContributor contributor = new ProduceRequestValidationFilterContributor();
+        ValidationConfig config = new ValidationConfig(true, List.of(), new RecordValidationRule(null, null));
+        Filter filter = contributor.getInstance("ProduceValidator", FilterConstructContext.wrap(config, () -> Executors.newScheduledThreadPool(1)));
+        assertThat(filter).isNotNull().isInstanceOf(ProduceValidationFilter.class);
+    }
+
+}
\ No newline at end of file
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 8c83e6c5..a5100c54 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
@@ -5,10 +5,13 @@
  */
 package io.kroxylicious.proxy.service;
 
+import java.util.concurrent.atomic.AtomicReference;
+
 import org.junit.jupiter.api.Test;
 
 import io.kroxylicious.proxy.config.BaseConfig;
 
+import static io.kroxylicious.proxy.service.Context.wrap;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
@@ -23,9 +26,9 @@ class BaseContributorTest {
 
     @Test
     void testDefaultConfigClass() {
-        BaseContributor.BaseContributorBuilder<Long> builder = BaseContributor.builder();
+        BaseContributor.BaseContributorBuilder<Long, Context> builder = BaseContributor.builder();
         builder.add("one", () -> 1L);
-        BaseContributor<Long> baseContributor = new BaseContributor<>(builder) {
+        BaseContributor<Long, Context> baseContributor = new BaseContributor<>(builder) {
         };
         Class<? extends BaseConfig> one = baseContributor.getConfigType("one");
         assertThat(one).isEqualTo(BaseConfig.class);
@@ -33,19 +36,45 @@ class BaseContributorTest {
 
     @Test
     void testSupplier() {
-        BaseContributor.BaseContributorBuilder<Long> builder = BaseContributor.builder();
+        BaseContributor.BaseContributorBuilder<Long, Context> builder = BaseContributor.builder();
         builder.add("one", () -> 1L);
-        BaseContributor<Long> baseContributor = new BaseContributor<>(builder) {
+        BaseContributor<Long, Context> baseContributor = new BaseContributor<>(builder) {
         };
-        Long instance = baseContributor.getInstance("one", new BaseConfig());
+        Long instance = baseContributor.getInstance("one", wrap(new BaseConfig()));
         assertThat(instance).isEqualTo(1L);
     }
 
+    @Test
+    void testNullConfigIsAllowed() {
+        BaseContributor.BaseContributorBuilder<Long, Context> builder = BaseContributor.builder();
+        builder.add("one", () -> 1L);
+        BaseContributor<Long, Context> baseContributor = new BaseContributor<>(builder) {
+        };
+        Long instance = baseContributor.getInstance("one", wrap(null));
+        assertThat(instance).isEqualTo(1L);
+    }
+
+    @Test
+    void testContextAndConfigFunction() {
+        BaseContributor.BaseContributorBuilder<Long, Context> builder = BaseContributor.builder();
+        AtomicReference<Context> contextRef = new AtomicReference<>();
+        builder.add("one", (context -> {
+            contextRef.set(context);
+            return 1L;
+        }));
+        BaseContributor<Long, Context> baseContributor = new BaseContributor<>(builder) {
+        };
+        Context context = wrap(new BaseConfig());
+        Long instance = baseContributor.getInstance("one", context);
+        assertThat(instance).isEqualTo(1L);
+        assertThat(contextRef).hasValue(context);
+    }
+
     @Test
     void testSpecifyingConfigType() {
-        BaseContributor.BaseContributorBuilder<Long> builder = BaseContributor.builder();
+        BaseContributor.BaseContributorBuilder<Long, Context> builder = BaseContributor.builder();
         builder.add("fromBaseConfig", LongConfig.class, baseConfig -> baseConfig.value);
-        BaseContributor<Long> baseContributor = new BaseContributor<>(builder) {
+        BaseContributor<Long, Context> baseContributor = new BaseContributor<>(builder) {
         };
         Class<? extends BaseConfig> configType = baseContributor.getConfigType("fromBaseConfig");
         assertThat(configType).isEqualTo(LongConfig.class);
@@ -53,23 +82,39 @@ class BaseContributorTest {
 
     @Test
     void testSpecifyingConfigTypeInstance() {
-        BaseContributor.BaseContributorBuilder<Long> builder = BaseContributor.builder();
+        BaseContributor.BaseContributorBuilder<Long, Context> builder = BaseContributor.builder();
         builder.add("fromBaseConfig", LongConfig.class, baseConfig -> baseConfig.value);
-        BaseContributor<Long> baseContributor = new BaseContributor<>(builder) {
+        BaseContributor<Long, Context> baseContributor = new BaseContributor<>(builder) {
+        };
+        Long instance = baseContributor.getInstance("fromBaseConfig", wrap(new LongConfig()));
+        assertThat(instance).isEqualTo(2L);
+    }
+
+    @Test
+    void testSpecifyingConfigTypeInstanceAndContext() {
+        BaseContributor.BaseContributorBuilder<Long, Context> builder = BaseContributor.builder();
+        AtomicReference<Context> contextRef = new AtomicReference<>();
+        builder.add("fromBaseConfig", LongConfig.class, (context, baseConfig) -> {
+            contextRef.set(context);
+            return baseConfig.value;
+        });
+        BaseContributor<Long, Context> baseContributor = new BaseContributor<>(builder) {
         };
-        Long instance = baseContributor.getInstance("fromBaseConfig", new LongConfig());
+        Context context = wrap(new LongConfig());
+        Long instance = baseContributor.getInstance("fromBaseConfig", context);
         assertThat(instance).isEqualTo(2L);
+        assertThat(contextRef).hasValue(context);
     }
 
     @Test
     void testFailsIfConfigNotAssignableToSpecifiedType() {
-        BaseContributor.BaseContributorBuilder<Long> builder = BaseContributor.builder();
+        BaseContributor.BaseContributorBuilder<Long, Context> builder = BaseContributor.builder();
         builder.add("fromBaseConfig", LongConfig.class, baseConfig -> baseConfig.value);
-        BaseContributor<Long> baseContributor = new BaseContributor<>(builder) {
+        BaseContributor<Long, Context> baseContributor = new BaseContributor<>(builder) {
         };
         AnotherConfig incompatibleConfig = new AnotherConfig();
         assertThatThrownBy(() -> {
-            baseContributor.getInstance("fromBaseConfig", incompatibleConfig);
+            baseContributor.getInstance("fromBaseConfig", wrap(incompatibleConfig));
         }).isInstanceOf(IllegalArgumentException.class);
     }
 
diff --git a/kroxylicious/src/test/java/io/kroxylicious/proxy/bootstrap/FilterChainFactoryTest.java b/kroxylicious/src/test/java/io/kroxylicious/proxy/bootstrap/FilterChainFactoryTest.java
index 4caa3072..8b3d6486 100644
--- a/kroxylicious/src/test/java/io/kroxylicious/proxy/bootstrap/FilterChainFactoryTest.java
+++ b/kroxylicious/src/test/java/io/kroxylicious/proxy/bootstrap/FilterChainFactoryTest.java
@@ -7,12 +7,22 @@
 package io.kroxylicious.proxy.bootstrap;
 
 import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
 
+import org.assertj.core.api.ListAssert;
 import org.junit.jupiter.api.Test;
 
 import io.kroxylicious.proxy.config.Configuration;
+import io.kroxylicious.proxy.config.FilterDefinition;
 import io.kroxylicious.proxy.filter.FilterAndInvoker;
+import io.kroxylicious.proxy.internal.filter.ExampleConfig;
+import io.kroxylicious.proxy.internal.filter.NettyFilterContext;
+import io.kroxylicious.proxy.internal.filter.TestFilter;
+import io.kroxylicious.proxy.internal.filter.TestFilterContributor;
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -20,18 +30,66 @@ class FilterChainFactoryTest {
 
     @Test
     void testNullFiltersInConfigResultsInEmptyList() {
+        ScheduledExecutorService eventLoop = Executors.newScheduledThreadPool(1);
         FilterChainFactory filterChainFactory = new FilterChainFactory(new Configuration(null, null, null, null, true));
-        List<FilterAndInvoker> filters = filterChainFactory.createFilters();
+        List<FilterAndInvoker> filters = filterChainFactory.createFilters(new NettyFilterContext(eventLoop));
         assertNotNull(filters, "Filters list should not be null");
         assertTrue(filters.isEmpty(), "Filters list should be empty");
     }
 
+    @Test
+    void testConfigurationNotNullable() {
+        assertThatThrownBy(() -> new FilterChainFactory(null)).isInstanceOf(NullPointerException.class);
+    }
+
     @Test
     void testEmptyFiltersInConfigResultsInEmptyList() {
+        ScheduledExecutorService eventLoop = Executors.newScheduledThreadPool(1);
         FilterChainFactory filterChainFactory = new FilterChainFactory(new Configuration(null, null, List.of(), null, true));
-        List<FilterAndInvoker> filters = filterChainFactory.createFilters();
+        List<FilterAndInvoker> filters = filterChainFactory.createFilters(new NettyFilterContext(eventLoop));
         assertNotNull(filters, "Filters list should not be null");
         assertTrue(filters.isEmpty(), "Filters list should be empty");
     }
 
-}
\ No newline at end of file
+    @Test
+    void testCreateFilter() {
+        ScheduledExecutorService eventLoop = Executors.newScheduledThreadPool(1);
+        ExampleConfig config = new ExampleConfig();
+        FilterChainFactory filterChainFactory = new FilterChainFactory(
+                new Configuration(null, null, List.of(new FilterDefinition(TestFilterContributor.SHORT_NAME_A, config)), null, true));
+        NettyFilterContext context = new NettyFilterContext(eventLoop);
+        List<FilterAndInvoker> filters = filterChainFactory.createFilters(context);
+        assertThat(filters).isNotNull().hasSize(1).first().extracting(FilterAndInvoker::filter).isInstanceOfSatisfying(TestFilter.class, testFilter -> {
+            assertThat(testFilter.getShortName()).isEqualTo(TestFilterContributor.SHORT_NAME_A);
+            assertThat(testFilter.getContext().getConfig()).isSameAs(config);
+            assertThat(testFilter.getContext().executors().eventLoop()).isSameAs(eventLoop);
+            assertThat(testFilter.getExampleConfig()).isSameAs(config);
+        });
+    }
+
+    @Test
+    void testCreateFilters() {
+        ScheduledExecutorService eventLoop = Executors.newScheduledThreadPool(1);
+        ExampleConfig config = new ExampleConfig();
+        FilterDefinition definitionA = new FilterDefinition(TestFilterContributor.SHORT_NAME_A, config);
+        FilterDefinition definitionB = new FilterDefinition(TestFilterContributor.SHORT_NAME_B, config);
+        List<FilterDefinition> filterDefinitions = List.of(definitionA, definitionB);
+        FilterChainFactory filterChainFactory = new FilterChainFactory(new Configuration(null, null, filterDefinitions, null, true));
+        NettyFilterContext context = new NettyFilterContext(eventLoop);
+        List<FilterAndInvoker> filters = filterChainFactory.createFilters(context);
+        ListAssert<FilterAndInvoker> listAssert = assertThat(filters).isNotNull().hasSize(2);
+        listAssert.element(0).extracting(FilterAndInvoker::filter).isInstanceOfSatisfying(TestFilter.class, testFilter -> {
+            assertThat(testFilter.getShortName()).isEqualTo(TestFilterContributor.SHORT_NAME_A);
+            assertThat(testFilter.getContext().getConfig()).isSameAs(config);
+            assertThat(testFilter.getContext().executors().eventLoop()).isSameAs(eventLoop);
+            assertThat(testFilter.getExampleConfig()).isSameAs(config);
+        });
+        listAssert.element(1).extracting(FilterAndInvoker::filter).isInstanceOfSatisfying(TestFilter.class, testFilter -> {
+            assertThat(testFilter.getShortName()).isEqualTo(TestFilterContributor.SHORT_NAME_B);
+            assertThat(testFilter.getContext().getConfig()).isSameAs(config);
+            assertThat(testFilter.getContext().executors().eventLoop()).isSameAs(eventLoop);
+            assertThat(testFilter.getExampleConfig()).isSameAs(config);
+        });
+    }
+
+}
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
new file mode 100644
index 00000000..5fcc63ef
--- /dev/null
+++ b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/ClusterNetworkAddressConfigProviderContributorManagerTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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/Config.java b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/Config.java
new file mode 100644
index 00000000..780606b4
--- /dev/null
+++ b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/Config.java
@@ -0,0 +1,12 @@
+/*
+ * 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 io.kroxylicious.proxy.config.BaseConfig;
+
+public class Config extends BaseConfig {
+}
diff --git a/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/TestClusterNetworkAddressConfigProvider.java b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/TestClusterNetworkAddressConfigProvider.java
new file mode 100644
index 00000000..d0bb3bdb
--- /dev/null
+++ b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/TestClusterNetworkAddressConfigProvider.java
@@ -0,0 +1,26 @@
+/*
+ * 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.testcontainers.shaded.org.apache.commons.lang3.NotImplementedException;
+
+import io.kroxylicious.proxy.config.BaseConfig;
+import io.kroxylicious.proxy.service.ClusterNetworkAddressConfigProvider;
+import io.kroxylicious.proxy.service.Context;
+import io.kroxylicious.proxy.service.HostPort;
+
+public record TestClusterNetworkAddressConfigProvider(String shortName, BaseConfig config, Context context) implements ClusterNetworkAddressConfigProvider {
+    @Override
+    public HostPort getClusterBootstrapAddress() {
+        throw new NotImplementedException("not implemented!");
+    }
+
+    @Override
+    public HostPort getBrokerAddress(int nodeId) throws IllegalArgumentException {
+        throw new NotImplementedException("not implemented!");
+    }
+}
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
new file mode 100644
index 00000000..a0c1d84f
--- /dev/null
+++ b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/TestClusterNetworkAddressConfigProviderContributor.java
@@ -0,0 +1,35 @@
+/*
+ * 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.Objects;
+
+import io.kroxylicious.proxy.clusternetworkaddressconfigprovider.ClusterNetworkAddressConfigProviderContributor;
+import io.kroxylicious.proxy.config.BaseConfig;
+import io.kroxylicious.proxy.service.ClusterNetworkAddressConfigProvider;
+import io.kroxylicious.proxy.service.Context;
+
+public class TestClusterNetworkAddressConfigProviderContributor implements ClusterNetworkAddressConfigProviderContributor {
+
+    public static final String SHORT_NAME = "test";
+
+    @Override
+    public Class<? extends BaseConfig> getConfigType(String shortName) {
+        if (!Objects.equals(shortName, SHORT_NAME)) {
+            return null;
+        }
+        return Config.class;
+    }
+
+    @Override
+    public ClusterNetworkAddressConfigProvider getInstance(String shortName, Context context) {
+        if (!Objects.equals(shortName, SHORT_NAME)) {
+            return null;
+        }
+        return new TestClusterNetworkAddressConfigProvider(shortName, context.getConfig(), context);
+    }
+}
diff --git a/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/filter/ExampleConfig.java b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/filter/ExampleConfig.java
new file mode 100644
index 00000000..cb5887f4
--- /dev/null
+++ b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/filter/ExampleConfig.java
@@ -0,0 +1,12 @@
+/*
+ * 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 io.kroxylicious.proxy.config.BaseConfig;
+
+public class ExampleConfig extends BaseConfig {
+}
diff --git a/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/filter/TestFilter.java b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/filter/TestFilter.java
new file mode 100644
index 00000000..a9ea3e17
--- /dev/null
+++ b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/filter/TestFilter.java
@@ -0,0 +1,47 @@
+/*
+ * 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.concurrent.CompletionStage;
+
+import org.apache.kafka.common.message.RequestHeaderData;
+import org.apache.kafka.common.protocol.ApiKeys;
+import org.apache.kafka.common.protocol.ApiMessage;
+
+import io.kroxylicious.proxy.filter.FilterConstructContext;
+import io.kroxylicious.proxy.filter.FilterContext;
+import io.kroxylicious.proxy.filter.RequestFilter;
+import io.kroxylicious.proxy.filter.RequestFilterResult;
+
+public class TestFilter implements RequestFilter {
+    private final String shortName;
+    private final FilterConstructContext context;
+    private final ExampleConfig exampleConfig;
+
+    public TestFilter(String shortName, FilterConstructContext context, ExampleConfig exampleConfig) {
+        this.shortName = shortName;
+        this.context = context;
+        this.exampleConfig = exampleConfig;
+    }
+
+    @Override
+    public CompletionStage<RequestFilterResult> onRequest(ApiKeys apiKey, RequestHeaderData header, ApiMessage request, FilterContext context) {
+        throw new IllegalStateException("not implemented!");
+    }
+
+    public String getShortName() {
+        return shortName;
+    }
+
+    public FilterConstructContext getContext() {
+        return context;
+    }
+
+    public ExampleConfig getExampleConfig() {
+        return exampleConfig;
+    }
+}
diff --git a/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/filter/TestFilterContributor.java b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/filter/TestFilterContributor.java
new file mode 100644
index 00000000..99c4e143
--- /dev/null
+++ b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/filter/TestFilterContributor.java
@@ -0,0 +1,24 @@
+/*
+ * 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 io.kroxylicious.proxy.filter.Filter;
+import io.kroxylicious.proxy.filter.FilterConstructContext;
+import io.kroxylicious.proxy.filter.FilterContributor;
+import io.kroxylicious.proxy.service.BaseContributor;
+
+public class TestFilterContributor extends BaseContributor<Filter, FilterConstructContext> implements FilterContributor {
+    public static final String SHORT_NAME_A = "TEST1";
+    public static final String SHORT_NAME_B = "TEST2";
+    public static final BaseContributorBuilder<Filter, FilterConstructContext> FILTERS = BaseContributor.<Filter, FilterConstructContext> builder()
+            .add(SHORT_NAME_A, ExampleConfig.class, (context, exampleConfig) -> new TestFilter(SHORT_NAME_A, context, exampleConfig))
+            .add(SHORT_NAME_B, ExampleConfig.class, (context, exampleConfig) -> new TestFilter(SHORT_NAME_B, context, exampleConfig));
+
+    public TestFilterContributor() {
+        super(FILTERS);
+    }
+}
diff --git a/kroxylicious/src/test/java/io/kroxylicious/proxy/micrometer/MicrometerConfigurationHookContributorManagerTest.java b/kroxylicious/src/test/java/io/kroxylicious/proxy/micrometer/MicrometerConfigurationHookContributorManagerTest.java
new file mode 100644
index 00000000..9b46afd0
--- /dev/null
+++ b/kroxylicious/src/test/java/io/kroxylicious/proxy/micrometer/MicrometerConfigurationHookContributorManagerTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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/TestHook.java b/kroxylicious/src/test/java/io/kroxylicious/proxy/micrometer/TestHook.java
new file mode 100644
index 00000000..068daf65
--- /dev/null
+++ b/kroxylicious/src/test/java/io/kroxylicious/proxy/micrometer/TestHook.java
@@ -0,0 +1,21 @@
+/*
+ * 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.testcontainers.shaded.org.apache.commons.lang3.NotImplementedException;
+
+import io.micrometer.core.instrument.MeterRegistry;
+
+import io.kroxylicious.proxy.config.BaseConfig;
+import io.kroxylicious.proxy.service.Context;
+
+public record TestHook(String shortName, BaseConfig config, Context context) implements MicrometerConfigurationHook {
+    @Override
+    public void configure(MeterRegistry targetRegistry) {
+        throw new NotImplementedException("not implemented!");
+    }
+}
diff --git a/kroxylicious/src/test/java/io/kroxylicious/proxy/micrometer/TestMicrometerConfigurationHookContributor.java b/kroxylicious/src/test/java/io/kroxylicious/proxy/micrometer/TestMicrometerConfigurationHookContributor.java
new file mode 100644
index 00000000..56c0fa82
--- /dev/null
+++ b/kroxylicious/src/test/java/io/kroxylicious/proxy/micrometer/TestMicrometerConfigurationHookContributor.java
@@ -0,0 +1,39 @@
+/*
+ * 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.Objects;
+
+import io.kroxylicious.proxy.config.BaseConfig;
+import io.kroxylicious.proxy.service.Context;
+
+public class TestMicrometerConfigurationHookContributor implements MicrometerConfigurationHookContributor {
+
+    public static class Config extends BaseConfig {
+
+    }
+
+    public static final String SHORT_NAME = "test";
+
+    @Override
+    public Class<? extends BaseConfig> getConfigType(String shortName) {
+        if (Objects.equals(shortName, SHORT_NAME)) {
+            return Config.class;
+        }
+        else {
+            return null;
+        }
+    }
+
+    @Override
+    public MicrometerConfigurationHook getInstance(String shortName, Context context) {
+        if (!Objects.equals(shortName, SHORT_NAME)) {
+            return null;
+        }
+        return new TestHook(shortName, context.getConfig(), context);
+    }
+}
diff --git a/kroxylicious/src/test/resources/META-INF/services/io.kroxylicious.proxy.clusternetworkaddressconfigprovider.ClusterNetworkAddressConfigProviderContributor b/kroxylicious/src/test/resources/META-INF/services/io.kroxylicious.proxy.clusternetworkaddressconfigprovider.ClusterNetworkAddressConfigProviderContributor
new file mode 100644
index 00000000..275ee0d4
--- /dev/null
+++ b/kroxylicious/src/test/resources/META-INF/services/io.kroxylicious.proxy.clusternetworkaddressconfigprovider.ClusterNetworkAddressConfigProviderContributor
@@ -0,0 +1,1 @@
+io.kroxylicious.proxy.internal.clusternetworkaddressconfigprovider.TestClusterNetworkAddressConfigProviderContributor
diff --git a/kroxylicious/src/test/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterContributor b/kroxylicious/src/test/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterContributor
new file mode 100644
index 00000000..8aec3a88
--- /dev/null
+++ b/kroxylicious/src/test/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterContributor
@@ -0,0 +1,1 @@
+io.kroxylicious.proxy.internal.filter.TestFilterContributor
diff --git a/kroxylicious/src/test/resources/META-INF/services/io.kroxylicious.proxy.micrometer.MicrometerConfigurationHookContributor b/kroxylicious/src/test/resources/META-INF/services/io.kroxylicious.proxy.micrometer.MicrometerConfigurationHookContributor
new file mode 100644
index 00000000..55f15f81
--- /dev/null
+++ b/kroxylicious/src/test/resources/META-INF/services/io.kroxylicious.proxy.micrometer.MicrometerConfigurationHookContributor
@@ -0,0 +1,1 @@
+io.kroxylicious.proxy.micrometer.TestMicrometerConfigurationHookContributor
\ No newline at end of file