Repository: kroxylicious/kroxylicious
Pull request #727 (2023-11-20 19:16:04)

Tease out simple transform filters into their own module
### Type of change

_Select the type of your PR_

- Refactoring

### Description

`ProduceRequestTransformationFilterFactory` and `FetchResponseTransformationFilterFactory` are used as transform filters to demonstrate the ability of Kroxylicious standalone.  `kroxylicious-runtime` is not the right home for them. They are also used some unit tests - which is questionable.

They should be moved to their own module and the unit test usages should be replaced with test specific classes.

### Additional Context

### Checklist

_Please go through this checklist and make sure all applicable tasks have been done_

- [ ] Write tests
- [x] Make sure all tests pass
- [ ] Review performance test results. Ensure that any degradations to performance numbers are understood and justified.
- [ ] Make sure all Sonar-Lint warnings are addressed or are justifiably ignored.
- [ ] Update documentation
- [ ] Reference relevant issue(s) and close them after merging
- [ ] For user facing changes, update CHANGELOG.md (remember to include changes affecting the API of the test artefacts too).


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

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

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

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

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

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

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

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

[![58.4%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/50-16px.png '58.4%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=727&metric=new_coverage&view=list) [58.4% Coverage](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=727&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=727&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=727&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)
k-wall: > I think this makes sense as a first step, but ultimately I think these should probably live in a `contrib` repository

Why a contrib directory?

I think this filter is something that we might choose to ship as part of the production artefact.

Apache Kafka ship `kafka-producer-perf-test` as part of their production artefact.   It's not intended for production use, but having it as part of the artefact gives users a really easy way to baseline their hardware.

I'm seeing `ProduceRequestTransformationFilter` etc in the same way.   The user could run up a test that allows them to compare without kroxylicious/with kroxylicious no filters/with kroxylicious + this filter to get a useful baseline of the overhead of introducing a proxy in their environment.  We could supply configurations that make running this sort of comparison really easy.
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=727)

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

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

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

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


SamBarker: >Why a contrib directory?

Note I didn't say a contrib directory I said repo. Moving to a new directory is as meaningless as you suggest. 

>I think this filter is something that we might choose to ship as part of the production artefact.

I don't see the utility in including this. The simple overheads test you outline would be just as easily satisfied by the *sample* filters. Even if we were to keep the simple filters just for that use case I think we'd want to use it as an opportunity to demonstrate how simple it is to include a 3rd party filter (obviously we are not there yet).

Actually this leads me to question why we keep the simple-transformation filters at all. You've done the important step of teasing out the test usages and replacing them so what value does the *kroxylicious-simple-transform* offer over *kroxylicious-sample*? 
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=727)

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

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


k-wall: > Actually this leads me to question why we keep the simple-transformation filters at all. You've done the important step of teasing out the test usages and replacing them so what value does the kroxylicious-simple-transform offer over kroxylicious-sample?

That's a good question. 

Perhaps `ProduceRequestTransformationFilter` / `FetchResponseTransformationFilter`  are superfluous?    It looks to be like their use in `FilterIT` could be substituted with `Sample*` relatively easily.  On the other hand, I've got a nagging feeling that doing that would blur use-cases.  Sample's role was to demonstrate how to implement a filter and filter writing best practice. 




1. I don't think we really get much value from the ByteBufferTransformation of a the simple transforms. Simple's string replace is good enough (for me).
2. It does serve to illustrate the pluggability mechansim 



robobario: If the simple transforms are only useful for integration testing then could they move into `io.kroxylicious.proxy.filter` in `kroxylicious-integration-tests`?

I think sample should remain a demonstration of how to set up a filter project and write filters, not be part of the tests proving Kroxylicious works.
k-wall: Ok, so let's keep this filter at least for now.  I would still like to move it out of `core` because it is confusing having its implementation bleed out its packages.  


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

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

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

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

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

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

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



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 774c08c4..9b0948f7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,13 +6,19 @@ Please enumerate **all user-facing** changes using format `<githib issue/pr numb
 
 ## 0.4.0
 
-
+* [#727](https://github.com/kroxylicious/kroxylicious/pull/727): Tease out simple transform filters into their own module
 * [#738](https://github.com/kroxylicious/kroxylicious/pull/738): Update to Kroxylicious Junit Ext 0.7.0
 * [#723](https://github.com/kroxylicious/kroxylicious/pull/723): Bump com.fasterxml.jackson:jackson-bom from 2.15.3 to 2.16.0 #723
 * [#724](https://github.com/kroxylicious/kroxylicious/pull/724): Bump io.netty.incubator:netty-incubator-transport-native-io_uring from 0.0.23.Final to 0.0.24.Final
 * [#725](https://github.com/kroxylicious/kroxylicious/pull/725): Bump io.netty:netty-bom from 4.1.100.Final to 4.1.101.Final #725
 * [#701](https://github.com/kroxylicious/kroxylicious/pull/701): Bump org.apache.logging.log4j:log4j-bom from 2.21.0 to 2.21.1 #701
 
+### Changes, deprecations and removals
+
+* The `ProduceRequestTransformationFilter` and `FetchResponseTransformationFilter` have been moved to their own module kroxylicious-simple-transform.
+  If you were depending on these filters, you must ensure that the kroxylicious-simple-transform JAR file is added to your classpath.  The
+  Javadoc of these classes has been updated to convey the fact that these filters are *not* intended for production use.
+
 ## 0.3.0
 
 * [#686](https://github.com/kroxylicious/kroxylicious/pull/686): Bump org.apache.logging.log4j:log4j-bom from 2.20.0 to 2.21.0.
diff --git a/Dockerfile b/Dockerfile
index 91e1930f..00739032 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -9,7 +9,7 @@ FROM registry.access.redhat.com/ubi9/openjdk-17:1.15 AS builder
 USER root
 WORKDIR /opt/kroxylicious
 COPY . .
-RUN ./mvnw -B clean verify -Pdist,withAdditionalFilters -Dquick -pl :kroxylicious-bom,:kroxylicious-app,:kroxylicious-multitenant,:kroxylicious-record-validation -am
+RUN ./mvnw -B clean verify -Pdist,withAdditionalFilters -Dquick -am -pl !kroxylicious-integration-tests
 USER 185
 FROM registry.access.redhat.com/ubi9/ubi-minimal:9.2
 
diff --git a/kroxylicious-api/src/main/java/io/kroxylicious/proxy/filter/FilterFactory.java b/kroxylicious-api/src/main/java/io/kroxylicious/proxy/filter/FilterFactory.java
index 5895d222..931de98f 100644
--- a/kroxylicious-api/src/main/java/io/kroxylicious/proxy/filter/FilterFactory.java
+++ b/kroxylicious-api/src/main/java/io/kroxylicious/proxy/filter/FilterFactory.java
@@ -28,7 +28,7 @@ public interface FilterFactory<C, I> {
      * This method may provide extra semantic validation of the config,
      * and returns some object (which may be the config, or some other object) which will be passed to {@link #createFilter(FilterFactoryContext, Object)}
      *
-     * @param context
+     * @param context context
      * @param config configuration
      * @throws PluginConfigurationException when the configuration is invalid
      */
diff --git a/kroxylicious-app/pom.xml b/kroxylicious-app/pom.xml
index 2d47a2ed..5647e799 100644
--- a/kroxylicious-app/pom.xml
+++ b/kroxylicious-app/pom.xml
@@ -195,5 +195,22 @@
                 </plugins>
             </build>
         </profile>
+        <profile>
+            <id>withAdditionalFilters</id>
+            <dependencies>
+                <dependency>
+                    <groupId>io.kroxylicious</groupId>
+                    <artifactId>kroxylicious-multitenant</artifactId>
+                </dependency>
+                <dependency>
+                    <groupId>io.kroxylicious</groupId>
+                    <artifactId>kroxylicious-record-validation</artifactId>
+                </dependency>
+                <dependency>
+                    <groupId>io.kroxylicious</groupId>
+                    <artifactId>kroxylicious-simple-transform</artifactId>
+                </dependency>
+            </dependencies>
+        </profile>
     </profiles>
 </project>
diff --git a/kroxylicious-bom/pom.xml b/kroxylicious-bom/pom.xml
index 54d7cb96..7f6ef5bc 100644
--- a/kroxylicious-bom/pom.xml
+++ b/kroxylicious-bom/pom.xml
@@ -135,6 +135,12 @@
                 <version>${project.version}</version>
             </dependency>
 
+            <dependency>
+                <groupId>io.kroxylicious</groupId>
+                <artifactId>kroxylicious-simple-transform</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+
             <!-- testing dependencies -->
             <!-- note scope is *not* set at this level -->
             <dependency>
diff --git a/kroxylicious-filters/kroxylicious-simple-transform/pom.xml b/kroxylicious-filters/kroxylicious-simple-transform/pom.xml
new file mode 100644
index 00000000..0031cdce
--- /dev/null
+++ b/kroxylicious-filters/kroxylicious-simple-transform/pom.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Copyright Kroxylicious Authors.
+
+    Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>io.kroxylicious</groupId>
+        <artifactId>kroxylicious-parent</artifactId>
+        <version>0.4.0-SNAPSHOT</version>
+        <relativePath>../../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>kroxylicious-simple-transform</artifactId>
+    <name>Simple filters</name>
+    <description>Several simple filters that exist for test purposes.</description>
+
+    <dependencies>
+        <!-- project dependencies - runtime and compile -->
+        <dependency>
+            <groupId>io.kroxylicious</groupId>
+            <artifactId>kroxylicious-api</artifactId>
+        </dependency>
+
+        <!-- project dependencies - test -->
+        <dependency>
+            <groupId>io.kroxylicious</groupId>
+            <artifactId>kroxylicious-filter-test-support</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <!-- third party dependencies - test -->
+        <dependency>
+            <groupId>com.github.spotbugs</groupId>
+            <artifactId>spotbugs-annotations</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-params</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-junit-jupiter</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
\ No newline at end of file
diff --git a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/ByteBufferTransformation.java b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/ByteBufferTransformation.java
similarity index 87%
rename from kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/ByteBufferTransformation.java
rename to kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/ByteBufferTransformation.java
index aa75e0d5..c7035b26 100644
--- a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/ByteBufferTransformation.java
+++ b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/ByteBufferTransformation.java
@@ -3,7 +3,7 @@
  *
  * 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;
+package io.kroxylicious.proxy.filter.simpletransform;
 
 import java.nio.ByteBuffer;
 
diff --git a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/ByteBufferTransformationFactory.java b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/ByteBufferTransformationFactory.java
similarity index 94%
rename from kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/ByteBufferTransformationFactory.java
rename to kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/ByteBufferTransformationFactory.java
index cc8be6b4..f8b8de84 100644
--- a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/ByteBufferTransformationFactory.java
+++ b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/ByteBufferTransformationFactory.java
@@ -4,7 +4,7 @@
  * 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;
+package io.kroxylicious.proxy.filter.simpletransform;
 
 import io.kroxylicious.proxy.plugin.PluginConfigurationException;
 
diff --git a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/FetchResponseTransformationFilter.java b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/FetchResponseTransformationFilter.java
similarity index 80%
rename from kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/FetchResponseTransformationFilter.java
rename to kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/FetchResponseTransformationFilter.java
index 8b08f5c6..b593c76d 100644
--- a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/FetchResponseTransformationFilter.java
+++ b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/FetchResponseTransformationFilter.java
@@ -4,9 +4,8 @@
  * 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;
+package io.kroxylicious.proxy.filter.simpletransform;
 
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CompletionStage;
@@ -24,6 +23,7 @@ import org.apache.kafka.common.record.MemoryRecords;
 import org.apache.kafka.common.record.MemoryRecordsBuilder;
 import org.apache.kafka.common.record.MutableRecordBatch;
 import org.apache.kafka.common.record.Record;
+import org.apache.kafka.common.record.RecordBatch;
 import org.apache.kafka.common.record.TimestampType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -31,11 +31,12 @@ import org.slf4j.LoggerFactory;
 import io.kroxylicious.proxy.filter.FetchResponseFilter;
 import io.kroxylicious.proxy.filter.FilterContext;
 import io.kroxylicious.proxy.filter.ResponseFilterResult;
-import io.kroxylicious.proxy.internal.util.MemoryRecordsHelper;
 
 /**
  * A filter for modifying the key/value/header/topic of {@link ApiKeys#FETCH} responses.
- */
+ * <p>
+ * <strong>Not intended to production use.</strong>
+ * </p> */
 public class FetchResponseTransformationFilter implements FetchResponseFilter {
 
     // Version 12 was the first version that uses topic ids.
@@ -93,18 +94,21 @@ public class FetchResponseTransformationFilter implements FetchResponseFilter {
     private void applyTransformation(FilterContext context, FetchResponseData responseData) {
         for (FetchResponseData.FetchableTopicResponse topicData : responseData.responses()) {
             for (FetchResponseData.PartitionData partitionData : topicData.partitions()) {
-                MemoryRecords records = (MemoryRecords) partitionData.records();
-                MemoryRecordsBuilder newRecords = MemoryRecordsHelper.builder(context.createByteBufferOutputStream(records.sizeInBytes()), CompressionType.NONE,
-                        TimestampType.CREATE_TIME, 0);
-
-                for (MutableRecordBatch batch : records.batches()) {
-                    for (Iterator<Record> batchRecords = batch.iterator(); batchRecords.hasNext();) {
-                        Record batchRecord = batchRecords.next();
-                        newRecords.append(batchRecord.timestamp(), batchRecord.key(), valueTransformation.transform(topicData.topic(), batchRecord.value()));
+                var records = (MemoryRecords) partitionData.records();
+                var stream = context.createByteBufferOutputStream(records.sizeInBytes());
+                try (var newRecords = new MemoryRecordsBuilder(stream, RecordBatch.CURRENT_MAGIC_VALUE, CompressionType.NONE, TimestampType.CREATE_TIME, 0,
+                        System.currentTimeMillis(), RecordBatch.NO_PRODUCER_ID, RecordBatch.NO_PRODUCER_EPOCH, RecordBatch.NO_SEQUENCE, false, false,
+                        RecordBatch.NO_PARTITION_LEADER_EPOCH,
+                        stream.remaining())) {
+
+                    for (MutableRecordBatch batch : records.batches()) {
+                        for (Record batchRecord : batch) {
+                            newRecords.append(batchRecord.timestamp(), batchRecord.key(), valueTransformation.transform(topicData.topic(), batchRecord.value()));
+                        }
                     }
-                }
 
-                partitionData.setRecords(newRecords.build());
+                    partitionData.setRecords(newRecords.build());
+                }
             }
         }
     }
diff --git a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/FetchResponseTransformationFilterFactory.java b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/FetchResponseTransformationFilterFactory.java
similarity index 91%
rename from kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/FetchResponseTransformationFilterFactory.java
rename to kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/FetchResponseTransformationFilterFactory.java
index ffa16300..9269b122 100644
--- a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/FetchResponseTransformationFilterFactory.java
+++ b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/FetchResponseTransformationFilterFactory.java
@@ -4,7 +4,7 @@
  * 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;
+package io.kroxylicious.proxy.filter.simpletransform;
 
 import java.util.Objects;
 
@@ -12,7 +12,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
 
 import io.kroxylicious.proxy.filter.FilterFactory;
 import io.kroxylicious.proxy.filter.FilterFactoryContext;
-import io.kroxylicious.proxy.internal.filter.FetchResponseTransformationFilterFactory.Config;
+import io.kroxylicious.proxy.filter.simpletransform.FetchResponseTransformationFilterFactory.Config;
 import io.kroxylicious.proxy.plugin.Plugin;
 import io.kroxylicious.proxy.plugin.PluginImplConfig;
 import io.kroxylicious.proxy.plugin.PluginImplName;
diff --git a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/ProduceRequestTransformationFilter.java b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/ProduceRequestTransformationFilter.java
similarity index 66%
rename from kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/ProduceRequestTransformationFilter.java
rename to kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/ProduceRequestTransformationFilter.java
index 700df30c..06604001 100644
--- a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/ProduceRequestTransformationFilter.java
+++ b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/ProduceRequestTransformationFilter.java
@@ -4,9 +4,8 @@
  * 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;
+package io.kroxylicious.proxy.filter.simpletransform;
 
-import java.util.Iterator;
 import java.util.concurrent.CompletionStage;
 
 import org.apache.kafka.common.message.ProduceRequestData;
@@ -17,15 +16,18 @@ import org.apache.kafka.common.record.MemoryRecords;
 import org.apache.kafka.common.record.MemoryRecordsBuilder;
 import org.apache.kafka.common.record.MutableRecordBatch;
 import org.apache.kafka.common.record.Record;
+import org.apache.kafka.common.record.RecordBatch;
 import org.apache.kafka.common.record.TimestampType;
 
 import io.kroxylicious.proxy.filter.FilterContext;
 import io.kroxylicious.proxy.filter.ProduceRequestFilter;
 import io.kroxylicious.proxy.filter.RequestFilterResult;
-import io.kroxylicious.proxy.internal.util.MemoryRecordsHelper;
 
 /**
  * A filter for modifying the key/value/header/topic of {@link ApiKeys#PRODUCE} requests.
+ * <p>
+ * <strong>Not intended to production use.</strong>
+ * </p>
  */
 public class ProduceRequestTransformationFilter implements ProduceRequestFilter {
 
@@ -50,17 +52,20 @@ public class ProduceRequestTransformationFilter implements ProduceRequestFilter
         req.topicData().forEach(topicData -> {
             for (ProduceRequestData.PartitionProduceData partitionData : topicData.partitionData()) {
                 MemoryRecords records = (MemoryRecords) partitionData.records();
-                MemoryRecordsBuilder newRecords = MemoryRecordsHelper.builder(ctx.createByteBufferOutputStream(records.sizeInBytes()), CompressionType.NONE,
-                        TimestampType.CREATE_TIME, 0);
+                var stream = ctx.createByteBufferOutputStream(records.sizeInBytes());
+                try (var newRecords = new MemoryRecordsBuilder(stream, RecordBatch.CURRENT_MAGIC_VALUE, CompressionType.NONE, TimestampType.CREATE_TIME, 0,
+                        System.currentTimeMillis(), RecordBatch.NO_PRODUCER_ID, RecordBatch.NO_PRODUCER_EPOCH, RecordBatch.NO_SEQUENCE, false, false,
+                        RecordBatch.NO_PARTITION_LEADER_EPOCH,
+                        stream.remaining())) {
 
-                for (MutableRecordBatch batch : records.batches()) {
-                    for (Iterator<Record> batchRecords = batch.iterator(); batchRecords.hasNext();) {
-                        Record batchRecord = batchRecords.next();
-                        newRecords.append(batchRecord.timestamp(), batchRecord.key(), valueTransformation.transform(topicData.name(), batchRecord.value()));
+                    for (MutableRecordBatch batch : records.batches()) {
+                        for (Record batchRecord : batch) {
+                            newRecords.append(batchRecord.timestamp(), batchRecord.key(), valueTransformation.transform(topicData.name(), batchRecord.value()));
+                        }
                     }
-                }
 
-                partitionData.setRecords(newRecords.build());
+                    partitionData.setRecords(newRecords.build());
+                }
             }
         });
     }
diff --git a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/ProduceRequestTransformationFilterFactory.java b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/ProduceRequestTransformationFilterFactory.java
similarity index 92%
rename from kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/ProduceRequestTransformationFilterFactory.java
rename to kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/ProduceRequestTransformationFilterFactory.java
index 2dbcc5ae..04fe5980 100644
--- a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/ProduceRequestTransformationFilterFactory.java
+++ b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/ProduceRequestTransformationFilterFactory.java
@@ -4,13 +4,13 @@
  * 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;
+package io.kroxylicious.proxy.filter.simpletransform;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 
 import io.kroxylicious.proxy.filter.FilterFactory;
 import io.kroxylicious.proxy.filter.FilterFactoryContext;
-import io.kroxylicious.proxy.internal.filter.ProduceRequestTransformationFilterFactory.Config;
+import io.kroxylicious.proxy.filter.simpletransform.ProduceRequestTransformationFilterFactory.Config;
 import io.kroxylicious.proxy.plugin.Plugin;
 import io.kroxylicious.proxy.plugin.PluginImplConfig;
 import io.kroxylicious.proxy.plugin.PluginImplName;
diff --git a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/UpperCasing.java b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/UpperCasing.java
similarity index 97%
rename from kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/UpperCasing.java
rename to kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/UpperCasing.java
index 51c393fe..3ac04fad 100644
--- a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/UpperCasing.java
+++ b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/UpperCasing.java
@@ -4,7 +4,7 @@
  * 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;
+package io.kroxylicious.proxy.filter.simpletransform;
 
 import java.nio.ByteBuffer;
 import java.nio.charset.Charset;
diff --git a/kroxylicious-filters/kroxylicious-simple-transform/src/main/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory b/kroxylicious-filters/kroxylicious-simple-transform/src/main/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory
new file mode 100644
index 00000000..d4d60bb8
--- /dev/null
+++ b/kroxylicious-filters/kroxylicious-simple-transform/src/main/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory
@@ -0,0 +1,2 @@
+io.kroxylicious.proxy.filter.simpletransform.ProduceRequestTransformationFilterFactory
+io.kroxylicious.proxy.filter.simpletransform.FetchResponseTransformationFilterFactory
diff --git a/kroxylicious-filters/kroxylicious-simple-transform/src/main/resources/META-INF/services/io.kroxylicious.proxy.filter.simpletransform.ByteBufferTransformationFactory b/kroxylicious-filters/kroxylicious-simple-transform/src/main/resources/META-INF/services/io.kroxylicious.proxy.filter.simpletransform.ByteBufferTransformationFactory
new file mode 100644
index 00000000..f69fc666
--- /dev/null
+++ b/kroxylicious-filters/kroxylicious-simple-transform/src/main/resources/META-INF/services/io.kroxylicious.proxy.filter.simpletransform.ByteBufferTransformationFactory
@@ -0,0 +1,7 @@
+#
+# Copyright Kroxylicious Authors.
+#
+# Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+#
+
+io.kroxylicious.proxy.filter.simpletransform.UpperCasing
\ No newline at end of file
diff --git a/kroxylicious-filters/pom.xml b/kroxylicious-filters/pom.xml
index 751c8741..6f3121e9 100644
--- a/kroxylicious-filters/pom.xml
+++ b/kroxylicious-filters/pom.xml
@@ -25,6 +25,7 @@
     <modules>
         <module>kroxylicious-multitenant</module>
         <module>kroxylicious-record-validation</module>
+        <module>kroxylicious-simple-transform</module>
     </modules>
 
     <build>
diff --git a/kroxylicious-runtime/src/main/resources/META-INF/services/io.kroxylicious.proxy.internal.filter.ByteBufferTransformationFactory b/kroxylicious-integration-test-support/src/test/resources/META-INF/services/io.kroxylicious.proxy.config.ExamplePluginFactory
similarity index 75%
rename from kroxylicious-runtime/src/main/resources/META-INF/services/io.kroxylicious.proxy.internal.filter.ByteBufferTransformationFactory
rename to kroxylicious-integration-test-support/src/test/resources/META-INF/services/io.kroxylicious.proxy.config.ExamplePluginFactory
index 50e4a843..3e18ac60 100644
--- a/kroxylicious-runtime/src/main/resources/META-INF/services/io.kroxylicious.proxy.internal.filter.ByteBufferTransformationFactory
+++ b/kroxylicious-integration-test-support/src/test/resources/META-INF/services/io.kroxylicious.proxy.config.ExamplePluginFactory
@@ -4,4 +4,4 @@
 # Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
 #
 
-io.kroxylicious.proxy.internal.filter.UpperCasing
\ No newline at end of file
+io.kroxylicious.proxy.config.ExamplePluginInstance
\ No newline at end of file
diff --git a/kroxylicious-integration-tests/src/test/resources/META-INF/services/io.kroxylicious.proxy.internal.filter.ByteBufferTransformationFactory b/kroxylicious-integration-tests/src/test/resources/META-INF/services/io.kroxylicious.proxy.filter.simpletransform.ByteBufferTransformationFactory
similarity index 100%
rename from kroxylicious-integration-tests/src/test/resources/META-INF/services/io.kroxylicious.proxy.internal.filter.ByteBufferTransformationFactory
rename to kroxylicious-integration-tests/src/test/resources/META-INF/services/io.kroxylicious.proxy.filter.simpletransform.ByteBufferTransformationFactory
diff --git a/kroxylicious-runtime/pom.xml b/kroxylicious-runtime/pom.xml
index 381b4f63..772aea5f 100644
--- a/kroxylicious-runtime/pom.xml
+++ b/kroxylicious-runtime/pom.xml
@@ -278,19 +278,6 @@
     </build>
 
     <profiles>
-        <profile>
-            <id>withAdditionalFilters</id>
-            <dependencies>
-                <dependency>
-                    <groupId>io.kroxylicious</groupId>
-                    <artifactId>kroxylicious-multitenant</artifactId>
-                </dependency>
-                <dependency>
-                    <groupId>io.kroxylicious</groupId>
-                    <artifactId>kroxylicious-record-validation</artifactId>
-                </dependency>
-            </dependencies>
-        </profile>
         <profile>
             <id>debug</id>
             <dependencyManagement>
diff --git a/kroxylicious-runtime/src/main/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory b/kroxylicious-runtime/src/main/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory
deleted file mode 100644
index c6fca29e..00000000
--- a/kroxylicious-runtime/src/main/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory
+++ /dev/null
@@ -1,2 +0,0 @@
-io.kroxylicious.proxy.internal.filter.ProduceRequestTransformationFilterFactory
-io.kroxylicious.proxy.internal.filter.FetchResponseTransformationFilterFactory


Test patch:
diff --git a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/FetchResponseTransformationFilterFactoryFilterTest.java b/kroxylicious-filters/kroxylicious-simple-transform/src/test/java/io/kroxylicious/proxy/filter/simpletransform/FetchResponseTransformationFilterFactoryFilterTest.java
similarity index 97%
rename from kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/FetchResponseTransformationFilterFactoryFilterTest.java
rename to kroxylicious-filters/kroxylicious-simple-transform/src/test/java/io/kroxylicious/proxy/filter/simpletransform/FetchResponseTransformationFilterFactoryFilterTest.java
index 7958d435..c4fa017d 100644
--- a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/FetchResponseTransformationFilterFactoryFilterTest.java
+++ b/kroxylicious-filters/kroxylicious-simple-transform/src/test/java/io/kroxylicious/proxy/filter/simpletransform/FetchResponseTransformationFilterFactoryFilterTest.java
@@ -4,7 +4,7 @@
  * 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;
+package io.kroxylicious.proxy.filter.simpletransform;
 
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
@@ -38,7 +38,6 @@ import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.mockito.Mock;
-import org.mockito.Mockito;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.mockito.stubbing.Answer;
 
@@ -47,7 +46,6 @@ import io.kroxylicious.proxy.filter.FilterFactoryContext;
 import io.kroxylicious.proxy.filter.ResponseFilterResult;
 import io.kroxylicious.proxy.filter.ResponseFilterResultBuilder;
 import io.kroxylicious.proxy.filter.filterresultbuilder.CloseOrTerminalStage;
-import io.kroxylicious.proxy.internal.filter.FetchResponseTransformationFilterFactory.Config;
 import io.kroxylicious.proxy.plugin.PluginConfigurationException;
 
 import edu.umd.cs.findbugs.annotations.NonNull;
@@ -57,6 +55,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.isA;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 @ExtendWith(MockitoExtension.class)
@@ -116,9 +115,9 @@ class FetchResponseTransformationFilterFactoryFilterTest {
         FetchResponseTransformationFilterFactory factory = new FetchResponseTransformationFilterFactory();
         assertThatThrownBy(() -> factory.initialize(null, null)).isInstanceOf(PluginConfigurationException.class)
                 .hasMessage(FetchResponseTransformationFilterFactory.class.getSimpleName() + " requires configuration, but config object is null");
-        FilterFactoryContext constructContext = Mockito.mock(FilterFactoryContext.class);
+        FilterFactoryContext constructContext = mock(FilterFactoryContext.class);
         doReturn(new UpperCasing()).when(constructContext).pluginInstance(any(), any());
-        Config config = new Config(UpperCasing.class.getName(),
+        FetchResponseTransformationFilterFactory.Config config = new FetchResponseTransformationFilterFactory.Config(UpperCasing.class.getName(),
                 new UpperCasing.Config("UTF-8"));
         assertThat(factory.createFilter(constructContext, config)).isInstanceOf(FetchResponseTransformationFilter.class);
     }
diff --git a/kroxylicious-filters/kroxylicious-simple-transform/src/test/java/io/kroxylicious/proxy/filter/simpletransform/ProduceRequestTransformationFilterFactoryFilterTest.java b/kroxylicious-filters/kroxylicious-simple-transform/src/test/java/io/kroxylicious/proxy/filter/simpletransform/ProduceRequestTransformationFilterFactoryFilterTest.java
new file mode 100644
index 00000000..11c3e6e1
--- /dev/null
+++ b/kroxylicious-filters/kroxylicious-simple-transform/src/test/java/io/kroxylicious/proxy/filter/simpletransform/ProduceRequestTransformationFilterFactoryFilterTest.java
@@ -0,0 +1,171 @@
+/*
+ * 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.simpletransform;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+import org.apache.kafka.common.message.ProduceRequestData;
+import org.apache.kafka.common.message.ProduceRequestData.PartitionProduceData;
+import org.apache.kafka.common.message.ProduceRequestData.TopicProduceData;
+import org.apache.kafka.common.message.RequestHeaderData;
+import org.apache.kafka.common.protocol.ApiMessage;
+import org.apache.kafka.common.record.CompressionType;
+import org.apache.kafka.common.record.MemoryRecords;
+import org.apache.kafka.common.record.MemoryRecordsBuilder;
+import org.apache.kafka.common.record.Record;
+import org.apache.kafka.common.record.RecordBatch;
+import org.apache.kafka.common.record.Records;
+import org.apache.kafka.common.record.TimestampType;
+import org.apache.kafka.common.utils.ByteBufferOutputStream;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.stubbing.Answer;
+
+import io.kroxylicious.proxy.filter.FilterContext;
+import io.kroxylicious.proxy.filter.FilterFactoryContext;
+import io.kroxylicious.proxy.filter.RequestFilterResult;
+import io.kroxylicious.proxy.plugin.PluginConfigurationException;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class ProduceRequestTransformationFilterFactoryFilterTest {
+
+    private static final String TOPIC_NAME = "mytopic";
+    private static final String ORIGINAL_RECORD_VALUE = "lowercasevalue";
+    private static final String EXPECTED_TRANSFORMED_RECORD_VALUE = ORIGINAL_RECORD_VALUE.toUpperCase(Locale.ROOT);
+    private static final String RECORD_KEY = "key";
+    private ProduceRequestTransformationFilter filter;
+    @Mock(strictness = Mock.Strictness.LENIENT)
+    FilterContext context;
+
+    @Mock(strictness = Mock.Strictness.LENIENT)
+    private RequestFilterResult responseFilterResult;
+
+    @Captor
+    private ArgumentCaptor<Integer> bufferInitialCapacity;
+
+    @Captor
+    private ArgumentCaptor<RequestHeaderData> requestHeaderDataCaptor;
+
+    @Captor
+    private ArgumentCaptor<ApiMessage> apiMessageCaptor;
+
+    @BeforeEach
+    void setUp() {
+        filter = new ProduceRequestTransformationFilter(new UpperCasing.Transformation(
+                new UpperCasing.Config("UTF-8")));
+
+        when(context.forwardRequest(requestHeaderDataCaptor.capture(), apiMessageCaptor.capture())).thenAnswer(
+                invocation -> CompletableFuture.completedStage(responseFilterResult));
+
+        when(responseFilterResult.message()).thenAnswer(invocation -> apiMessageCaptor.getValue());
+        when(responseFilterResult.header()).thenAnswer(invocation -> requestHeaderDataCaptor.getValue());
+
+        when(context.createByteBufferOutputStream(bufferInitialCapacity.capture())).thenAnswer(
+                (Answer<ByteBufferOutputStream>) invocation -> {
+                    Object[] args = invocation.getArguments();
+                    Integer size = (Integer) args[0];
+                    return new ByteBufferOutputStream(size);
+                });
+    }
+
+    @Test
+    void testFactory() {
+        var factory = new ProduceRequestTransformationFilterFactory();
+        assertThatThrownBy(() -> factory.initialize(null, null)).isInstanceOf(PluginConfigurationException.class)
+                .hasMessage(ProduceRequestTransformationFilterFactory.class.getSimpleName() + " requires configuration, but config object is null");
+        FilterFactoryContext constructContext = mock(FilterFactoryContext.class);
+        doReturn(new UpperCasing()).when(constructContext).pluginInstance(any(), any());
+        var config = new ProduceRequestTransformationFilterFactory.Config(UpperCasing.class.getName(),
+                new UpperCasing.Config("UTF-8"));
+        assertThat(factory.createFilter(constructContext, config)).isInstanceOf(ProduceRequestTransformationFilter.class);
+    }
+
+    @Test
+    void filterProduceRequest() throws Exception {
+
+        var produceRequest = new ProduceRequestData();
+        produceRequest.topicData().add(createTopicProduceDataWithOneRecord(RECORD_KEY, ORIGINAL_RECORD_VALUE).setName(TOPIC_NAME));
+
+        var stage = filter.onProduceRequest(produceRequest.apiKey(), new RequestHeaderData(), produceRequest, context);
+        assertThat(stage).isCompleted();
+
+        var filteredRequest = (ProduceRequestData) stage.toCompletableFuture().get().message();
+
+        // verify that the response now has the topic name
+        assertThat(filteredRequest.topicData())
+                .withFailMessage("expected same number of topics in the request")
+                .hasSameSizeAs(produceRequest.topicData())
+                .withFailMessage("expected topic request to have been augmented with topic name")
+                .anyMatch(ftr -> Objects.equals(ftr.name(), TOPIC_NAME));
+
+        var filteredRecords = requestToRecordStream(filteredRequest).toList();
+        assertThat(filteredRecords)
+                .withFailMessage("unexpected number of records in the filtered request")
+                .hasSize(1);
+
+        var filteredRecord = filteredRecords.get(0);
+        assertThat(decodeUtf8Value(filteredRecord))
+                .withFailMessage("expected record value to have been transformed")
+                .isEqualTo(EXPECTED_TRANSFORMED_RECORD_VALUE);
+    }
+
+    private Stream<Record> requestToRecordStream(ProduceRequestData filteredResponse) {
+        return Stream.of(filteredResponse.topicData())
+                .flatMap(Collection::stream)
+                .map(TopicProduceData::partitionData)
+                .flatMap(Collection::stream)
+                .map(PartitionProduceData::records)
+                .map(Records.class::cast)
+                .map(Records::records)
+                .map(Iterable::spliterator)
+                .flatMap(si -> StreamSupport.stream(si, false));
+    }
+
+    @NonNull
+    private static TopicProduceData createTopicProduceDataWithOneRecord(String key, String value) {
+        var topicProduceData = new TopicProduceData();
+        var partitionData = new PartitionProduceData();
+        partitionData.setRecords(buildOneRecord(key, value));
+        topicProduceData.partitionData().add(partitionData);
+        return topicProduceData;
+    }
+
+    private static MemoryRecords buildOneRecord(String key, String value) {
+        try (MemoryRecordsBuilder builder = MemoryRecords.builder(ByteBuffer.allocate(1024), RecordBatch.CURRENT_MAGIC_VALUE,
+                CompressionType.NONE, TimestampType.CREATE_TIME, 0L, System.currentTimeMillis())) {
+            builder.append(0L, key.getBytes(), value.getBytes());
+            return builder.build();
+        }
+    }
+
+    @NonNull
+    private String decodeUtf8Value(Record record) {
+        return StandardCharsets.UTF_8.decode(record.value()).toString();
+    }
+
+}
diff --git a/kroxylicious-integration-test-support/pom.xml b/kroxylicious-integration-test-support/pom.xml
index f77e7a46..3c0c7b65 100644
--- a/kroxylicious-integration-test-support/pom.xml
+++ b/kroxylicious-integration-test-support/pom.xml
@@ -21,7 +21,7 @@
     </parent>
     <artifactId>kroxylicious-integration-test-support</artifactId>
     <name>Integration test support</name>
-    <description>Support code to simplfy writing integration tests for filters</description>
+    <description>Support code to simplify writing integration tests for filters</description>
 
     <dependencies>
 
diff --git a/kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ConfigurationTest.java b/kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ConfigurationTest.java
index 68acbaff..b8603841 100644
--- a/kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ConfigurationTest.java
+++ b/kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ConfigurationTest.java
@@ -6,6 +6,7 @@
 
 package io.kroxylicious.proxy.config;
 
+import java.util.Map;
 import java.util.stream.Stream;
 
 import org.junit.jupiter.params.ParameterizedTest;
@@ -17,9 +18,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
 import com.flipkart.zjsonpatch.JsonDiff;
 
-import io.kroxylicious.proxy.internal.filter.ProduceRequestTransformationFilterFactory;
-import io.kroxylicious.proxy.internal.filter.UpperCasing;
-
 import static org.assertj.core.api.Assertions.assertThat;
 
 class ConfigurationTest {
@@ -34,18 +32,18 @@ class ConfigurationTest {
                         useIoUring: true"""),
                 Arguments.of("With filter",
                         new ConfigurationBuilder()
-                                .addToFilters(new FilterDefinitionBuilder(ProduceRequestTransformationFilterFactory.class.getSimpleName())
-                                        .withConfig("transformation", UpperCasing.class.getName(),
-                                                "transformationConfig", new UpperCasing.Config("UTF-8"))
+                                .addToFilters(new FilterDefinitionBuilder(ExampleFilterFactory.class.getSimpleName())
+                                        .withConfig("examplePlugin", "ExamplePluginInstance",
+                                                "examplePluginConfig", Map.of("pluginKey", "pluginValue"))
                                         .build())
                                 .build(),
                         """
                                     filters:
-                                    - type: ProduceRequestTransformationFilterFactory
+                                    - type: ExampleFilterFactory
                                       config:
-                                        transformation: "io.kroxylicious.proxy.internal.filter.UpperCasing"
-                                        transformationConfig:
-                                          charset: UTF-8
+                                        examplePlugin: ExamplePluginInstance
+                                        examplePluginConfig:
+                                          pluginKey: pluginValue
                                 """),
                 Arguments.of("With Virtual Cluster",
                         new ConfigurationBuilder()
diff --git a/kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ExampleFilterFactory.java b/kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ExampleFilterFactory.java
new file mode 100644
index 00000000..abc77537
--- /dev/null
+++ b/kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ExampleFilterFactory.java
@@ -0,0 +1,42 @@
+/*
+ * 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.config;
+
+import java.util.Objects;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import io.kroxylicious.proxy.filter.Filter;
+import io.kroxylicious.proxy.filter.FilterFactory;
+import io.kroxylicious.proxy.filter.FilterFactoryContext;
+import io.kroxylicious.proxy.plugin.Plugin;
+import io.kroxylicious.proxy.plugin.PluginImplConfig;
+import io.kroxylicious.proxy.plugin.PluginImplName;
+import io.kroxylicious.proxy.plugin.Plugins;
+
+import static org.junit.jupiter.api.Assertions.fail;
+
+@Plugin(configType = ExampleFilterFactory.Config.class)
+public class ExampleFilterFactory implements FilterFactory<ExampleFilterFactory.Config, ExampleFilterFactory.Config> {
+
+    public record Config(@PluginImplName(ExamplePluginFactory.class) @JsonProperty(required = true) String examplePlugin,
+                         @PluginImplConfig(implNameProperty = "examplePlugin") Object examplePluginConfig) {}
+
+    @Override
+    public Config initialize(FilterFactoryContext context, Config config) {
+        var factory = context.pluginInstance(ExampleFilterFactory.class, config.examplePlugin());
+        Objects.requireNonNull(factory, "Violated contract of FilterCreationContext");
+        return Plugins.requireConfig(this, config);
+    }
+
+    @Override
+    public Filter createFilter(FilterFactoryContext context, Config configuration) {
+        fail("unexpected call");
+        return null;
+    }
+
+}
diff --git a/kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ExamplePluginFactory.java b/kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ExamplePluginFactory.java
new file mode 100644
index 00000000..c7b252dd
--- /dev/null
+++ b/kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ExamplePluginFactory.java
@@ -0,0 +1,29 @@
+/*
+ * 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.config;
+
+import io.kroxylicious.proxy.plugin.PluginConfigurationException;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+
+public interface ExamplePluginFactory<C> {
+
+    @NonNull
+    default C requireConfig(C config) {
+        if (config == null) {
+            throw new PluginConfigurationException(this.getClass().getSimpleName() + " requires configuration, but config object is null");
+        }
+        return config;
+    }
+
+    ExamplePlugin createExamplePlugin(C configuration);
+
+    @FunctionalInterface
+    interface ExamplePlugin {
+        void foo();
+    }
+}
diff --git a/kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ExamplePluginInstance.java b/kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ExamplePluginInstance.java
new file mode 100644
index 00000000..ee74bccf
--- /dev/null
+++ b/kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ExamplePluginInstance.java
@@ -0,0 +1,23 @@
+/*
+ * 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.config;
+
+import io.kroxylicious.proxy.plugin.Plugin;
+
+import static org.junit.jupiter.api.Assertions.fail;
+
+@Plugin(configType = ExamplePluginInstance.Config.class)
+public class ExamplePluginInstance implements ExamplePluginFactory<ExamplePluginInstance.Config> {
+
+    @Override
+    public ExamplePlugin createExamplePlugin(Config configuration) {
+        fail("unexpected call");
+        return null;
+    }
+
+    public record Config(String pluginKey) {}
+}
diff --git a/kroxylicious-integration-test-support/src/test/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory b/kroxylicious-integration-test-support/src/test/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory
new file mode 100644
index 00000000..dd952e2f
--- /dev/null
+++ b/kroxylicious-integration-test-support/src/test/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory
@@ -0,0 +1,6 @@
+#
+# Copyright Kroxylicious Authors.
+#
+# Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+#
+io.kroxylicious.proxy.config.ExampleFilterFactory
\ No newline at end of file
diff --git a/kroxylicious-integration-tests/pom.xml b/kroxylicious-integration-tests/pom.xml
index 98284cfa..1121f57f 100644
--- a/kroxylicious-integration-tests/pom.xml
+++ b/kroxylicious-integration-tests/pom.xml
@@ -48,6 +48,11 @@
             <artifactId>kroxylicious-record-validation</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>io.kroxylicious</groupId>
+            <artifactId>kroxylicious-simple-transform</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>io.kroxylicious.testing</groupId>
             <artifactId>testing-api</artifactId>
diff --git a/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/FilterIT.java b/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/FilterIT.java
index 1c801e96..2a8390a9 100644
--- a/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/FilterIT.java
+++ b/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/FilterIT.java
@@ -56,8 +56,8 @@ import io.kroxylicious.proxy.filter.RejectingCreateTopicFilter;
 import io.kroxylicious.proxy.filter.RejectingCreateTopicFilterFactory;
 import io.kroxylicious.proxy.filter.RequestResponseMarkingFilter;
 import io.kroxylicious.proxy.filter.RequestResponseMarkingFilterFactory;
-import io.kroxylicious.proxy.internal.filter.FetchResponseTransformationFilterFactory;
-import io.kroxylicious.proxy.internal.filter.ProduceRequestTransformationFilterFactory;
+import io.kroxylicious.proxy.filter.simpletransform.FetchResponseTransformationFilterFactory;
+import io.kroxylicious.proxy.filter.simpletransform.ProduceRequestTransformationFilterFactory;
 import io.kroxylicious.test.Request;
 import io.kroxylicious.test.Response;
 import io.kroxylicious.test.ResponsePayload;
diff --git a/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/TestDecoderFactory.java b/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/TestDecoderFactory.java
index fadea1a6..7dfe1182 100644
--- a/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/TestDecoderFactory.java
+++ b/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/TestDecoderFactory.java
@@ -8,8 +8,8 @@ package io.kroxylicious.proxy;
 
 import java.nio.ByteBuffer;
 
-import io.kroxylicious.proxy.internal.filter.ByteBufferTransformation;
-import io.kroxylicious.proxy.internal.filter.ByteBufferTransformationFactory;
+import io.kroxylicious.proxy.filter.simpletransform.ByteBufferTransformation;
+import io.kroxylicious.proxy.filter.simpletransform.ByteBufferTransformationFactory;
 import io.kroxylicious.proxy.plugin.Plugin;
 import io.kroxylicious.proxy.plugin.PluginConfigurationException;
 
@@ -26,7 +26,7 @@ public class TestDecoderFactory implements ByteBufferTransformationFactory<Void>
         return new TestDecoder();
     }
 
-    public class TestDecoder implements ByteBufferTransformation {
+    public static class TestDecoder implements ByteBufferTransformation {
 
         @Override
         public ByteBuffer transform(String topicName, ByteBuffer in) {
diff --git a/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/TestEncoderFactory.java b/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/TestEncoderFactory.java
index 2f3f7fee..266b21af 100644
--- a/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/TestEncoderFactory.java
+++ b/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/TestEncoderFactory.java
@@ -8,8 +8,8 @@ package io.kroxylicious.proxy;
 
 import java.nio.ByteBuffer;
 
-import io.kroxylicious.proxy.internal.filter.ByteBufferTransformation;
-import io.kroxylicious.proxy.internal.filter.ByteBufferTransformationFactory;
+import io.kroxylicious.proxy.filter.simpletransform.ByteBufferTransformation;
+import io.kroxylicious.proxy.filter.simpletransform.ByteBufferTransformationFactory;
 import io.kroxylicious.proxy.plugin.Plugin;
 import io.kroxylicious.proxy.plugin.PluginConfigurationException;
 
diff --git a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/KafkaProxyTest.java b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/KafkaProxyTest.java
index 8f913333..5f458420 100644
--- a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/KafkaProxyTest.java
+++ b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/KafkaProxyTest.java
@@ -27,24 +27,23 @@ class KafkaProxyTest {
     @Test
     void shouldFailToStartIfRequireFilterConfigIsMissing() throws Exception {
         var config = """
-                    virtualClusters:
-                        demo1:
-                            targetCluster:
-                              bootstrap_servers: kafka.example:1234
-                            clusterNetworkAddressConfigProvider:
-                              type: PortPerBrokerClusterNetworkAddressConfigProvider
-                              config:
-                                bootstrapAddress: localhost:9192
-                                brokerStartPort: 9193
-                                numberOfBrokerPorts: 2
-                    filters:
-                       - type: ProduceRequestTransformationFilterFactory
+                   virtualClusters:
+                     demo1:
+                       targetCluster:
+                         bootstrap_servers: kafka.example:1234
+                       clusterNetworkAddressConfigProvider:
+                         type: PortPerBrokerClusterNetworkAddressConfigProvider
+                         config:
+                           bootstrapAddress: localhost:9192
+                           numberOfBrokerPorts: 1
+                   filters:
+                   - type: RequiresConfigFactory
                 """;
-        ConfigParser configParser = new ConfigParser();
+        var configParser = new ConfigParser();
         try (var kafkaProxy = new KafkaProxy(configParser, configParser.parseConfiguration(config))) {
             assertThatThrownBy(kafkaProxy::startup).isInstanceOf(PluginConfigurationException.class)
                     .hasMessage(
-                            "Exception initializing filter factory ProduceRequestTransformationFilterFactory with config null: ProduceRequestTransformationFilterFactory requires configuration, but config object is null");
+                            "Exception initializing filter factory RequiresConfigFactory with config null: RequiresConfigFactory requires configuration, but config object is null");
         }
     }
 
diff --git a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/config/ConfigParserTest.java b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/config/ConfigParserTest.java
index 2f623ecd..48d46c44 100644
--- a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/config/ConfigParserTest.java
+++ b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/config/ConfigParserTest.java
@@ -26,14 +26,13 @@ import com.flipkart.zjsonpatch.JsonDiff;
 import io.kroxylicious.proxy.config.admin.AdminHttpConfiguration;
 import io.kroxylicious.proxy.filter.FilterFactory;
 import io.kroxylicious.proxy.internal.clusternetworkaddressconfigprovider.PortPerBrokerClusterNetworkAddressConfigProvider.PortPerBrokerClusterNetworkAddressConfigProviderConfig;
-import io.kroxylicious.proxy.internal.filter.ByteBufferTransformationFactory;
 import io.kroxylicious.proxy.internal.filter.ConstructorInjectionConfig;
+import io.kroxylicious.proxy.internal.filter.ExamplePluginFactory;
 import io.kroxylicious.proxy.internal.filter.FactoryMethodConfig;
 import io.kroxylicious.proxy.internal.filter.FieldInjectionConfig;
-import io.kroxylicious.proxy.internal.filter.ProduceRequestTransformationFilterFactory;
+import io.kroxylicious.proxy.internal.filter.NestedPluginConfigFactory;
 import io.kroxylicious.proxy.internal.filter.RecordConfig;
 import io.kroxylicious.proxy.internal.filter.SetterInjectionConfig;
-import io.kroxylicious.proxy.internal.filter.UpperCasing;
 import io.kroxylicious.proxy.plugin.UnknownPluginInstanceException;
 import io.kroxylicious.proxy.service.HostPort;
 
@@ -47,7 +46,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 class ConfigParserTest {
     private static final ObjectMapper MAPPER = new ObjectMapper(new YAMLFactory());
     // Given
-    private ConfigParser configParser = new ConfigParser();
+    private final ConfigParser configParser = new ConfigParser();
 
     public static Stream<Arguments> yamlDeserializeSerializeFidelity() {
         return Stream.of(Arguments.of("Top level flags", """
@@ -103,11 +102,7 @@ class ConfigParserTest {
 
                 Arguments.of("Filters", """
                         filters:
-                        - type: ProduceRequestTransformationFilterFactory
-                          config:
-                            transformation: io.kroxylicious.proxy.internal.filter.UpperCasing
-                            transformationConfig:
-                              charset: UTF-8
+                        - type: TestFilterFactory
                         """),
                 Arguments.of("Admin", """
                         adminHttp:
@@ -249,25 +244,22 @@ class ConfigParserTest {
         ConfigParser cp = new ConfigParser();
         var config = cp.parseConfiguration("""
                 filters:
-                - type: ProduceRequestTransformationFilterFactory
+                - type: NestedPluginConfigFactory
                   config:
-                    transformation: UpperCasing
-                    transformationConfig:
-                      charset: UTF-8
+                    examplePlugin: ExamplePluginInstance
                       """);
         assertThat(config.filters()).hasSize(1);
 
         FilterDefinition fd = config.filters().get(0);
-        assertEquals("ProduceRequestTransformationFilterFactory", fd.type());
+        assertThat(fd.type()).isEqualTo("NestedPluginConfigFactory");
         FilterFactory<?, ?> ff = cp.pluginFactory(FilterFactory.class).pluginInstance(fd.type());
         assertThat(ff).isNotNull();
-        assertThat(fd.config()).isInstanceOf(ProduceRequestTransformationFilterFactory.Config.class);
+        assertThat(fd.config()).isInstanceOf(NestedPluginConfigFactory.NestedPluginConfig.class);
 
-        var prtc = (ProduceRequestTransformationFilterFactory.Config) fd.config();
-        assertThat(prtc.transformationConfig()).isInstanceOf(UpperCasing.Config.class);
-        assertEquals("UpperCasing", prtc.transformation());
-        ByteBufferTransformationFactory<?> tm = cp.pluginFactory(ByteBufferTransformationFactory.class).pluginInstance(prtc.transformation());
-        assertThat(tm).isNotNull();
+        var npc = (NestedPluginConfigFactory.NestedPluginConfig) fd.config();
+        assertThat(npc.examplePlugin()).isEqualTo("ExamplePluginInstance");
+        var ep = cp.pluginFactory(ExamplePluginFactory.class).pluginInstance(npc.examplePlugin());
+        assertThat(ep).isNotNull();
     }
 
     @Test
@@ -275,17 +267,16 @@ class ConfigParserTest {
         ConfigParser cp = new ConfigParser();
         var iae = assertThrows(IllegalArgumentException.class, () -> cp.parseConfiguration("""
                 filters:
-                - type: ProduceRequestTransformationFilterFactory
+                - type: NestedPluginConfigFactory
                   config:
-                    transformation: NotAKnownPlugin
-                    transformationConfig:
-                      charset: UTF-8
+                    examplePlugin: NotAKnownPlugin
+
                       """));
         var vie = assertInstanceOf(ValueInstantiationException.class, iae.getCause());
         var upie = assertInstanceOf(UnknownPluginInstanceException.class, vie.getCause());
-        assertEquals("Unknown io.kroxylicious.proxy.internal.filter.ByteBufferTransformationFactory plugin instance for "
+        assertEquals("Unknown io.kroxylicious.proxy.internal.filter.ExamplePluginFactory plugin instance for "
                 + "name 'NotAKnownPlugin'. "
-                + "Known plugin instances are [UpperCasing, io.kroxylicious.proxy.internal.filter.UpperCasing]. "
+                + "Known plugin instances are [ExamplePluginInstance, io.kroxylicious.proxy.internal.filter.ExamplePluginInstance]. "
                 + "Plugins must be loadable by java.util.ServiceLoader and annotated with "
                 + "@Plugin.",
                 upie.getMessage());
diff --git a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/ExamplePluginFactory.java b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/ExamplePluginFactory.java
new file mode 100644
index 00000000..bcf32e7a
--- /dev/null
+++ b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/ExamplePluginFactory.java
@@ -0,0 +1,17 @@
+/*
+ * 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;
+
+public interface ExamplePluginFactory<C> {
+
+    ExamplePlugin createExamplePlugin(C configuration);
+
+    @FunctionalInterface
+    interface ExamplePlugin {
+        void foo();
+    }
+}
diff --git a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/ExamplePluginInstance.java b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/ExamplePluginInstance.java
new file mode 100644
index 00000000..c1ef0d3a
--- /dev/null
+++ b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/ExamplePluginInstance.java
@@ -0,0 +1,23 @@
+/*
+ * 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.plugin.Plugin;
+
+import static org.junit.jupiter.api.Assertions.fail;
+
+@Plugin(configType = ExamplePluginInstance.Config.class)
+public class ExamplePluginInstance implements ExamplePluginFactory<ExamplePluginInstance.Config> {
+    public record Config(String myConfig) {}
+
+    @Override
+    public ExamplePlugin createExamplePlugin(Config configuration) {
+        fail("unexpected call");
+        return null;
+    }
+
+}
diff --git a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/FilterTest.java b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/FilterTest.java
index 9c896bc5..30060764 100644
--- a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/FilterTest.java
+++ b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/FilterTest.java
@@ -7,28 +7,28 @@
 package io.kroxylicious.proxy.internal.filter;
 
 import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
+import org.mockito.Mock;
 
 import io.kroxylicious.proxy.filter.FilterFactoryContext;
 import io.kroxylicious.proxy.plugin.PluginConfigurationException;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doReturn;
 
 class FilterTest {
+    private final RequiresConfigFactory factory = new RequiresConfigFactory();
+    @Mock
+    FilterFactoryContext constructContext;
+
     @Test
-    void testFactory() {
-        ProduceRequestTransformationFilterFactory factory = new ProduceRequestTransformationFilterFactory();
+    void detectsMissingConfig() {
         assertThatThrownBy(() -> factory.initialize(null, null)).isInstanceOf(PluginConfigurationException.class)
-                .hasMessage(ProduceRequestTransformationFilterFactory.class.getSimpleName() + " requires configuration, but config object is null");
-        FilterFactoryContext constructContext = Mockito.mock(FilterFactoryContext.class);
-        doReturn(new UpperCasing()).when(constructContext).pluginInstance(eq(ByteBufferTransformationFactory.class), any());
-        ProduceRequestTransformationFilterFactory.Config config = new ProduceRequestTransformationFilterFactory.Config(
-                UpperCasing.class.getName(),
-                new UpperCasing.Config("UTF-8"));
-        assertThat(factory.createFilter(constructContext, config)).isInstanceOf(ProduceRequestTransformationFilter.class);
+                .hasMessage(RequiresConfigFactory.class.getSimpleName() + " requires configuration, but config object is null");
+    }
+
+    @Test
+    void createFilter() {
+        var config = new ExampleConfig();
+        assertThat(factory.createFilter(constructContext, config)).isInstanceOf(RequiresConfigFactory.Filter.class);
     }
 }
diff --git a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/NestedPluginConfigFactory.java b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/NestedPluginConfigFactory.java
new file mode 100644
index 00000000..3cdb3335
--- /dev/null
+++ b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/NestedPluginConfigFactory.java
@@ -0,0 +1,42 @@
+/*
+ * 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.Objects;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import io.kroxylicious.proxy.filter.Filter;
+import io.kroxylicious.proxy.filter.FilterFactory;
+import io.kroxylicious.proxy.filter.FilterFactoryContext;
+import io.kroxylicious.proxy.plugin.Plugin;
+import io.kroxylicious.proxy.plugin.PluginImplConfig;
+import io.kroxylicious.proxy.plugin.PluginImplName;
+import io.kroxylicious.proxy.plugin.Plugins;
+
+import static org.junit.jupiter.api.Assertions.fail;
+
+@Plugin(configType = NestedPluginConfigFactory.NestedPluginConfig.class)
+public class NestedPluginConfigFactory implements FilterFactory<NestedPluginConfigFactory.NestedPluginConfig, NestedPluginConfigFactory.NestedPluginConfig> {
+
+    @Override
+    public NestedPluginConfig initialize(FilterFactoryContext context, NestedPluginConfig config) {
+        var factory = context.pluginInstance(ExamplePluginFactory.class, config.examplePlugin());
+        Objects.requireNonNull(factory, "Violated contract of FilterCreationContext");
+        return Plugins.requireConfig(this, config);
+    }
+
+    @Override
+    public Filter createFilter(FilterFactoryContext context, NestedPluginConfig configuration) {
+        fail("unexpected call");
+        return null;
+    }
+
+    public record NestedPluginConfig(@PluginImplName(ExamplePluginFactory.class) @JsonProperty(required = true) String examplePlugin,
+                                     @PluginImplConfig(implNameProperty = "examplePlugin") Object examplePluginConfig) {}
+
+}
diff --git a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/OptionalConfigFactory.java b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/OptionalConfigFactory.java
index 91f000e0..4d6618b1 100644
--- a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/OptionalConfigFactory.java
+++ b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/OptionalConfigFactory.java
@@ -17,7 +17,9 @@ import io.kroxylicious.proxy.filter.FilterFactory;
 import io.kroxylicious.proxy.filter.FilterFactoryContext;
 import io.kroxylicious.proxy.filter.RequestFilter;
 import io.kroxylicious.proxy.filter.RequestFilterResult;
+import io.kroxylicious.proxy.plugin.Plugin;
 
+@Plugin(configType = ExampleConfig.class)
 public class OptionalConfigFactory implements FilterFactory<ExampleConfig, ExampleConfig> {
 
     @Override
diff --git a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/RequiresConfigFactory.java b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/RequiresConfigFactory.java
index f83e8a86..7739c9f5 100644
--- a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/RequiresConfigFactory.java
+++ b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/RequiresConfigFactory.java
@@ -17,8 +17,10 @@ import io.kroxylicious.proxy.filter.FilterFactory;
 import io.kroxylicious.proxy.filter.FilterFactoryContext;
 import io.kroxylicious.proxy.filter.RequestFilter;
 import io.kroxylicious.proxy.filter.RequestFilterResult;
+import io.kroxylicious.proxy.plugin.Plugin;
 import io.kroxylicious.proxy.plugin.Plugins;
 
+@Plugin(configType = ExampleConfig.class)
 public class RequiresConfigFactory implements FilterFactory<ExampleConfig, ExampleConfig> {
 
     @Override
diff --git a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/TestFilterFactory.java b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/TestFilterFactory.java
index c0fcd9ac..7cdd0c08 100644
--- a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/TestFilterFactory.java
+++ b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/TestFilterFactory.java
@@ -17,8 +17,10 @@ import io.kroxylicious.proxy.filter.FilterFactory;
 import io.kroxylicious.proxy.filter.FilterFactoryContext;
 import io.kroxylicious.proxy.filter.RequestFilter;
 import io.kroxylicious.proxy.filter.RequestFilterResult;
+import io.kroxylicious.proxy.plugin.Plugin;
 import io.kroxylicious.proxy.plugin.Plugins;
 
+@Plugin(configType = ExampleConfig.class)
 public class TestFilterFactory implements FilterFactory<ExampleConfig, ExampleConfig> {
 
     @Override
diff --git a/kroxylicious-runtime/src/test/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory b/kroxylicious-runtime/src/test/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory
index f009d352..091ef372 100644
--- a/kroxylicious-runtime/src/test/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory
+++ b/kroxylicious-runtime/src/test/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory
@@ -1,3 +1,8 @@
+#
+# Copyright Kroxylicious Authors.
+#
+# Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+#
 io.kroxylicious.proxy.internal.filter.TestFilterFactory
 io.kroxylicious.proxy.internal.filter.OptionalConfigFactory
 io.kroxylicious.proxy.internal.filter.RequiresConfigFactory
@@ -6,4 +11,5 @@ io.kroxylicious.proxy.internal.filter.FieldInjection
 io.kroxylicious.proxy.internal.filter.Record
 io.kroxylicious.proxy.internal.filter.SetterInjection
 io.kroxylicious.proxy.internal.filter.FactoryMethod
-io.kroxylicious.proxy.internal.filter.MissingPluginImplName
\ No newline at end of file
+io.kroxylicious.proxy.internal.filter.MissingPluginImplName
+io.kroxylicious.proxy.internal.filter.NestedPluginConfigFactory
diff --git a/kroxylicious-runtime/src/test/resources/META-INF/services/io.kroxylicious.proxy.internal.filter.ExamplePluginFactory b/kroxylicious-runtime/src/test/resources/META-INF/services/io.kroxylicious.proxy.internal.filter.ExamplePluginFactory
new file mode 100644
index 00000000..3bb0a53c
--- /dev/null
+++ b/kroxylicious-runtime/src/test/resources/META-INF/services/io.kroxylicious.proxy.internal.filter.ExamplePluginFactory
@@ -0,0 +1,6 @@
+#
+# Copyright Kroxylicious Authors.
+#
+# Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+#
+io.kroxylicious.proxy.internal.filter.ExamplePluginInstance
\ No newline at end of file