Repository: kroxylicious/kroxylicious
Pull request #154 (2023-02-26 20:34:43)

Add pull request and issue templates
Inspired (copy + paste) by the Strimzi project templates, thanks!


Patch:
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000..76b0b720
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,31 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: bug
+assignees: ''
+
+---
+
+_Please use this to only for bug reports_. For questions or when you need help, you can use the [GitHub Discussions](https://github.com/kroxylicious/kroxylicious/discussions)
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Configure kroxylicious like this ...
+2. Run command '....'
+3. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**YAML files and logs**
+
+Attach or copy paste your kroxylicious configuration YAML (with sensitive details
+like broker address redacted).
+Attach or copy and paste relevant logs.
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..3e2306d8
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: true
+contact_links:
+  - name: Questions and Discussions
+    url: https://github.com/kroxylicious/kroxylicious/discussions
+    about: Please ask and discuss questions here.
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md
new file mode 100644
index 00000000..022a283d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/documentation.md
@@ -0,0 +1,14 @@
+---
+name: Documentation
+about: Report a bug or suggest an idea for our documentation
+title: ''
+labels: documentation
+assignees: ''
+
+---
+
+**Suggestion / Problem**
+Describe your problem or suggestion
+
+**Documentation Link**
+Provide a link to the documentation where the bug / suggestion should be applied
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000..11fc491e
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: enhancement
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 00000000..009a0d2d
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,16 @@
+### Type of change
+
+_Select the type of your PR_
+
+- Bugfix
+- Enhancement / new feature
+- Refactoring
+- Documentation
+
+### Description
+
+_Please describe your pull request_
+
+### Additional Context
+
+_Why are you making this pull request?_
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..d4cd6577
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+  - package-ecosystem: "maven" # See documentation for possible values
+    directory: "/" # Location of package manifests
+    schedule:
+      interval: "weekly"
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index 957c9d4a..835296c5 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -28,22 +28,22 @@ jobs:
 
     steps:
       - name: 'Check out repository'
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
         with:
           submodules: 'true'
 
       - name: 'Set up Java'
-        uses: actions/setup-java@v2
+        uses: actions/setup-java@v3
         with:
           java-version: 19
           distribution: 'zulu'
 
       - name: 'Cache Maven packages'
-        uses: actions/cache@v2
+        uses: actions/cache@v3
         with:
           path: ~/.m2
           key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
           restore-keys: ${{ runner.os }}-m2
 
       - name: 'Build example service'
-        run: mvn -B clean verify
+        run: mvn -B clean verify -Pci
diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml
new file mode 100644
index 00000000..e5d1b2c9
--- /dev/null
+++ b/.github/workflows/performance.yml
@@ -0,0 +1,165 @@
+#
+#  Licensed to the Apache Software Foundation (ASF) under one or more
+#  contributor license agreements. See the NOTICE file distributed with
+#  this work for additional information regarding copyright ownership.
+#  The ASF licenses this file to You under the Apache License, Version 2.0
+#  (the "License"); you may not use this file except in compliance with
+#  the License. You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#
+name: Performance
+
+on:
+  issue_comment:
+  workflow_dispatch:
+    inputs:
+      kafkaVersion:
+        description: 'The version of kafka to be used'
+        required: true
+        default: '3.4.0'
+
+jobs:
+  pr_comment:
+    name: Benchmark by comment
+    if: ${{ (github.event.issue.pull_request && contains(github.event.comment.body, '/perf')) }}
+    runs-on: ubuntu-22.04
+
+    steps:
+      - uses: xt0rted/pull-request-comment-branch@v1
+        id: comment-branch
+
+      - name: 'Check out repository'
+        uses: actions/checkout@v3
+        with:
+          ref: ${{ steps.comment-branch.outputs.head_ref }}
+          submodules: 'true'
+
+      - name: 'Set up Java'
+        uses: actions/setup-java@v3
+        with:
+          java-version: 19
+          distribution: 'zulu'
+
+      - name: 'Set Kafka Version'
+        run: |
+          echo "KAFKA_VERSION=3.4.0" >> $GITHUB_ENV
+
+      - name: 'Cache Maven packages'
+        uses: actions/cache@v3
+        with:
+          path: ~/.m2
+          key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
+          restore-keys: ${{ runner.os }}-m2
+
+      - name: 'Build kroxy service'
+        run: mvn -B clean verify -Pdist -Dquick
+
+      - name: 'Download kafka'
+        run: mkdir kafka_2.13; wget -q https://downloads.apache.org/kafka/${{ env.KAFKA_VERSION }}/kafka_2.13-${{ env.KAFKA_VERSION }}.tgz; tar xf kafka_2.13-${{ env.KAFKA_VERSION }}.tgz -C kafka_2.13 --strip-components=1
+
+      - name: 'Run zookeeper'
+        run: cd kafka_2.13; bin/zookeeper-server-start.sh config/zookeeper.properties &
+
+      - name: 'Run kafka'
+        run: cd kafka_2.13; bin/kafka-server-start.sh config/server.properties &
+
+      - name: 'Run kroxy'
+        run: java -jar kroxylicious/target/kroxylicious-1.0-SNAPSHOT.jar --config kroxylicious/example-proxy-config.yml &
+
+      - name: 'Run warm-up'
+        run: cd kafka_2.13; bin/kafka-producer-perf-test.sh --topic perf-test --throughput -1 --num-records 1000000 --record-size 1024 --producer-props acks=all bootstrap.servers=localhost:9192
+
+      - name: 'Run performance tests'
+        run: kafka_2.13/bin/kafka-producer-perf-test.sh --topic perf-test --throughput -1 --num-records 10000000 --record-size 1024 --producer-props acks=all bootstrap.servers=localhost:9192 | tee output.txt
+
+      - name: 'Create json file'
+        run: ./benchmark_output_to_json.sh output.txt output.json
+
+      - name: 'Store benchmark result'
+        uses: benchmark-action/github-action-benchmark@v1
+        with:
+          name: kafka producer perf test Benchmark
+          tool: 'customSmallerIsBetter'
+          output-file-path: output.json
+          gh-pages-branch: gh-pages
+          benchmark-data-dir-path: .benchmark
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          auto-push: true
+          alert-threshold: '150%'
+          comment-on-alert: true
+          fail-on-alert: true
+          alert-comment-cc-users: '@kroxylicious/developers'
+
+  workflow:
+    name: Benchmark manually launched
+    if: ${{ contains(github.event_name, 'workflow') }}
+    runs-on: ubuntu-22.04
+
+    steps:
+      - name: 'Check out repository'
+        uses: actions/checkout@v3
+        with:
+          submodules: 'true'
+
+      - name: 'Set up Java'
+        uses: actions/setup-java@v3
+        with:
+          java-version: 19
+          distribution: 'zulu'
+
+      - name: 'Set Kafka Version'
+        run: |
+            echo "KAFKA_VERSION=${{ github.event.inputs.kafkaVersion }}" >> $GITHUB_ENV
+
+      - name: 'Cache Maven packages'
+        uses: actions/cache@v3
+        with:
+          path: ~/.m2
+          key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
+          restore-keys: ${{ runner.os }}-m2
+
+      - name: 'Build kroxy service'
+        run: mvn -B clean verify -Pdist -Dquick
+
+      - name: 'Download kafka'
+        run: mkdir kafka_2.13; wget -q https://downloads.apache.org/kafka/${{ env.KAFKA_VERSION }}/kafka_2.13-${{ env.KAFKA_VERSION }}.tgz; tar xf kafka_2.13-${{ env.KAFKA_VERSION }}.tgz -C kafka_2.13 --strip-components=1
+
+      - name: 'Run zookeeper'
+        run: cd kafka_2.13; bin/zookeeper-server-start.sh config/zookeeper.properties &
+
+      - name: 'Run kafka'
+        run: cd kafka_2.13; bin/kafka-server-start.sh config/server.properties &
+
+      - name: 'Run kroxy'
+        run: java -jar kroxylicious/target/kroxylicious-1.0-SNAPSHOT.jar --config kroxylicious/example-proxy-config.yml &
+
+      - name: 'Run warm-up'
+        run: cd kafka_2.13; bin/kafka-producer-perf-test.sh --topic perf-test --throughput -1 --num-records 1000000 --record-size 1024 --producer-props acks=all bootstrap.servers=localhost:9192
+
+      - name: 'Run performance tests'
+        run: kafka_2.13/bin/kafka-producer-perf-test.sh --topic perf-test --throughput -1 --num-records 10000000 --record-size 1024 --producer-props acks=all bootstrap.servers=localhost:9192 | tee output.txt
+
+      - name: 'Create json file'
+        run: ./benchmark_output_to_json.sh output.txt output.json
+
+      - name: 'Store benchmark result'
+        uses: benchmark-action/github-action-benchmark@v1
+        with:
+          name: kafka producer perf test Benchmark
+          tool: 'customSmallerIsBetter'
+          output-file-path: output.json
+          gh-pages-branch: gh-pages
+          benchmark-data-dir-path: .benchmark
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          auto-push: true
+          alert-threshold: '150%'
+          comment-on-alert: false
+          fail-on-alert: true
+          alert-comment-cc-users: '@kroxylicious/developers'
diff --git a/README.md b/README.md
index 8bae7d0c..8aded739 100644
--- a/README.md
+++ b/README.md
@@ -3,42 +3,6 @@
 Kroxylicious is an exploration for building a Kafka protocol proxy,
 addressing use cases such as multi-tenancy, schema validation, or encryption.
 
-# Setting Up in Windows Using WSL
-### For installing WSL (Skip the First '4' steps if already done)
-1. Enable the Windows Subsystem for Linux feature: To enable WSL, you need to enable the Windows Subsystem for Linux feature in the Windows Features dialog.
-
-
-2. Install a Linux distribution from the Microsoft Store. The rest of these instructions assume a distribution (such as Ubuntu) which uses `apt` for package management, but the process should be similar for distributions using other package managers/ such as Fedora/`dnf`.
-
-3. Launch the Linux distribution and Configure the Linux environment: After launching the Linux distribution, you can configure the environment by creating a user account and setting the password.
-
-With these steps, you should now have WSL installed and configured on your Windows system. 
-
-For further assistance please see the [Microsoft documentation](https://learn.microsoft.com/en-us/windows/wsl/install)
-
-4. Open the WSL window.
-
-
-5. Install Latest Java version by typing 
-```bash
-java --version
-```
-and Choose the version (preferably v18), it should look like:
-```bash
-sudo apt install openjdk-18-jre-headless
-```
-6. Update the packages using
-```bash
-  sudo apt update
-  sudo apt upgrade
-```
-
-7. Navigate to kroxylicious folder using :
-```bash
- cd kroxylicious
- ```
-
-
 ## Build
 
 Java, version 11 or newer, and Apache Maven are required for building this project.
@@ -128,6 +92,20 @@ The output will be in `target/html/master.html`.
 
 See [benchmarking.md](benchmarking.md) for information on running basic performance tests for this proxy.
 
+### GitHub action for performance
+When a PR is created and the performance tests are needed, the following comment shall be added into the PR:
+
+```
+/perf
+```
+
+It will launch the `Performance` build, compare the results with the previous execution and store the results in the gh-pages repo under the `.benchmark` directory.
+
+In case the results are worse than expected (150% threshold) and alert will be shown into the commit and an email will be sent to the kroxylicious developers
+to inform them.
+
+**NOTE**: The alert cannot be currently shown in the PR, but the [upstream project](https://github.com/benchmark-action/github-action-benchmark) is working on adding this functionality.
+
 ## Architecture Monitoring
 
 This project uses [Deptective 🕵](https://github.com/moditect/deptective/) for monitoring its architecture and fails any violations,
@@ -149,7 +127,7 @@ $ dot -Tpng kroxylicious/target/generated-sources/annotations/deptective.dot > k
 
 ## IntelliJ setup
 
-Currently the project uses JDK 11 for the actual code and JDK 19 for tests. 
+Currently, the project uses JDK 11 for the actual code and JDK 19 for tests. 
 IntelliJ needs to be configured to use the "new Workspace Model" in order for it to understand this
 (and to be useful for things like debugging):
 
@@ -160,3 +138,50 @@ IntelliJ needs to be configured to use the "new Workspace Model" in order for it
 ## License
 
 This code base is available under the Apache License, version 2.
+
+## Setting Up in Windows Using WSL
+While Kroxylicious is a java application we've had reports of issues running the build natively on Windows and thus suggest using the Windows Subsystem for Linux (WSL) for development.
+### Installing WSL
+
+1. Enable the Windows Subsystem for Linux feature: To enable WSL, you need to enable the Windows Subsystem for Linux
+   feature in the Windows Features dialog.
+2. Install a Linux distribution from the Microsoft Store. The rest of these instructions assume a distribution (such as
+   Ubuntu) which uses `apt` for package management, but the process should be similar for distributions using other
+   package managers/ such as Fedora/`dnf`.
+3. Launch the Linux distribution and Configure the Linux environment: After launching the Linux distribution, you can
+   configure the environment by creating a user account and setting the password.
+   With these steps, you should now have WSL installed and configured on your Windows system.
+   For further assistance please see the [Microsoft documentation](https://learn.microsoft.com/en-us/windows/wsl/install)
+### Ensure appropriate tooling available
+1. Open the WSL window.
+2. Update the packages using
+    ```bash
+    sudo apt update
+    sudo apt upgrade
+    ```
+3. 
+    1. Check the Java version by typing
+      ```bash
+      java --version
+      ```
+      Expect output similar to: 
+      ```bash
+      > java --version
+   openjdk 19.0.2 2023-01-17
+   OpenJDK Runtime Environment Temurin-19.0.2+7 (build 19.0.2+7)
+   OpenJDK 64-Bit Server VM Temurin-19.0.2+7 (build 19.0.2+7, mixed mode, sharing)
+   ```
+    2. Update if needed: sample update command like:
+    ```bash
+    sudo apt update
+    sudo apt upgrade
+    sudo apt install openjdk-18-jre-headless
+    ```
+4. Ensure GIT is available
+   1. ```bash
+      git --version
+      ```
+      Expect a version string similar to `git version 2.37.1 (Apple Git-137.1)`
+   2. Follow the [WSL-git tutorial](https://learn.microsoft.com/en-us/windows/wsl/tutorials/wsl-git) if needed.
+5. Checkout Kroxylicious to `${kroxylicious-checkout}`
+6. Build & develop following the [standard build](#Build) instructions
\ No newline at end of file
diff --git a/api/kroxylicious-api/pom.xml b/api/kroxylicious-api/pom.xml
new file mode 100644
index 00000000..41324858
--- /dev/null
+++ b/api/kroxylicious-api/pom.xml
@@ -0,0 +1,45 @@
+<?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>1.0-SNAPSHOT</version>
+        <relativePath>../../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>kroxylicious-api</artifactId>
+    <version>0.0.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <name>Kroxylicious API</name>
+    <description>generic APIs used throughout Kroxylicious</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-annotations</artifactId>
+        </dependency>
+        <!-- Test dependencies -->
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/BaseConfig.java b/api/kroxylicious-api/src/main/java/io/kroxylicious/proxy/config/BaseConfig.java
similarity index 100%
rename from kroxylicious/src/main/java/io/kroxylicious/proxy/config/BaseConfig.java
rename to api/kroxylicious-api/src/main/java/io/kroxylicious/proxy/config/BaseConfig.java
diff --git a/api/kroxylicious-api/src/main/java/io/kroxylicious/proxy/config/ProxyConfig.java b/api/kroxylicious-api/src/main/java/io/kroxylicious/proxy/config/ProxyConfig.java
new file mode 100644
index 00000000..3035aead
--- /dev/null
+++ b/api/kroxylicious-api/src/main/java/io/kroxylicious/proxy/config/ProxyConfig.java
@@ -0,0 +1,10 @@
+/*
+ * 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;
+
+public interface ProxyConfig {
+    String address();
+}
diff --git a/api/kroxylicious-filter-api/pom.xml b/api/kroxylicious-filter-api/pom.xml
new file mode 100644
index 00000000..3863adc4
--- /dev/null
+++ b/api/kroxylicious-filter-api/pom.xml
@@ -0,0 +1,136 @@
+<?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>1.0-SNAPSHOT</version>
+        <relativePath>../../pom.xml</relativePath>
+    </parent>
+    
+    <artifactId>kroxylicious-filter-api</artifactId>
+    <version>0.0.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <properties>
+        <libs.dir>libs</libs.dir>
+    </properties>
+
+    <name>Kroxylicious Filter API</name>
+    <description>The public interfaces, classes, etc required to author custom filters and configure Kroxylicious to use them</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.kroxylicious</groupId>
+            <artifactId>kroxylicious-api</artifactId>
+            <version>0.0.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>io.micrometer</groupId>
+            <artifactId>micrometer-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.kafka</groupId>
+            <artifactId>kafka-clients</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-annotations</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>unpack-message-specs</id>
+                        <goals>
+                            <goal>unpack</goal>
+                        </goals>
+                        <phase>generate-sources</phase>
+                        <configuration>
+                            <artifact>org.apache.kafka:kafka-clients:${kafka.version}</artifact>
+                            <includes>common/message/*.json</includes>
+                            <outputDirectory>${project.build.directory}/message-specs</outputDirectory>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <!-- This actually shouldn't be needed, as the plug-in itself adds that directory as a source directory;
+                 Unfortunately, Eclipse/M2E doesn't reliably pick it up without this helper plug-in -->
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>build-helper-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>add-source</id>
+                        <phase>generate-sources</phase>
+                        <goals>
+                            <goal>add-source</goal>
+                        </goals>
+                        <configuration>
+                            <sources>
+                                <source>${project.build.directory}/generated-sources/krpc</source>
+                            </sources>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>io.kroxylicious</groupId>
+                <artifactId>kroxylicious-krpc-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>generate-filters</id>
+                        <goals>
+                            <goal>generate-single</goal>
+                        </goals>
+                        <phase>generate-sources</phase>
+                        <configuration>
+                            <messageSpecDirectory>${project.build.directory}/message-specs/common/message</messageSpecDirectory>
+                            <messageSpecFilter>*{Request,Response}.json</messageSpecFilter>
+                            <templateDirectory>${project.basedir}/src/main/templates</templateDirectory>
+                            <templateNames>Kproxy/Filter.ftl</templateNames>
+                            <outputFilePattern>${messageSpecName}Filter.java</outputFilePattern>
+                            <outputPackage>io.kroxylicious.proxy.filter</outputPackage>
+                            <outputDirectory>${project.build.directory}/generated-sources/krpc</outputDirectory>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>generate-request-filters</id>
+                        <goals>
+                            <goal>generate-multi</goal>
+                        </goals>
+                        <phase>generate-sources</phase>
+                        <configuration>
+                            <messageSpecDirectory>${project.build.directory}/message-specs/common/message</messageSpecDirectory>
+                            <messageSpecFilter>*{Request,Response}.json</messageSpecFilter>
+                            <templateDirectory>${project.basedir}/src/main/templates</templateDirectory>
+                            <templateNames>Kproxy/KrpcFilter.ftl</templateNames>
+                            <outputFilePattern>${templateName}.java</outputFilePattern>
+                            <outputPackage>io.kroxylicious.proxy.filter</outputPackage>
+                            <outputDirectory>${project.build.directory}/generated-sources/krpc</outputDirectory>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/filter/FilterContributor.java b/api/kroxylicious-filter-api/src/main/java/io/kroxylicious/proxy/filter/FilterContributor.java
similarity index 100%
rename from kroxylicious/src/main/java/io/kroxylicious/proxy/filter/FilterContributor.java
rename to api/kroxylicious-filter-api/src/main/java/io/kroxylicious/proxy/filter/FilterContributor.java
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/filter/KrpcFilterContext.java b/api/kroxylicious-filter-api/src/main/java/io/kroxylicious/proxy/filter/KrpcFilterContext.java
similarity index 85%
rename from kroxylicious/src/main/java/io/kroxylicious/proxy/filter/KrpcFilterContext.java
rename to api/kroxylicious-filter-api/src/main/java/io/kroxylicious/proxy/filter/KrpcFilterContext.java
index 20b33552..8c004d81 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/filter/KrpcFilterContext.java
+++ b/api/kroxylicious-filter-api/src/main/java/io/kroxylicious/proxy/filter/KrpcFilterContext.java
@@ -8,8 +8,7 @@ package io.kroxylicious.proxy.filter;
 import java.util.concurrent.CompletionStage;
 
 import org.apache.kafka.common.protocol.ApiMessage;
-
-import io.netty.buffer.ByteBuf;
+import org.apache.kafka.common.utils.ByteBufferOutputStream;
 
 /**
  * A context to allow filters to interact with other filters and the pipeline.
@@ -21,12 +20,12 @@ public interface KrpcFilterContext {
     String channelDescriptor();
 
     /**
-     * Allocate a ByteBuffer of the given capacity.
-     * The buffer will be deallocated when the request processing is completed
+     * Create a ByteBufferOutputStream of the given capacity.
+     * The backing buffer will be deallocated when the request processing is completed
      * @param initialCapacity The initial capacity of the buffer.
-     * @return The allocated buffer
+     * @return The allocated ByteBufferOutputStream
      */
-    ByteBuf allocate(int initialCapacity);
+    ByteBufferOutputStream createByteBufferOutputStream(int initialCapacity);
 
     /**
      * @return the SNI hostname provided by the client.  Will be null if the client is
diff --git a/kroxylicious/src/main/templates/Kproxy/Filter.ftl b/api/kroxylicious-filter-api/src/main/templates/Kproxy/Filter.ftl
similarity index 100%
rename from kroxylicious/src/main/templates/Kproxy/Filter.ftl
rename to api/kroxylicious-filter-api/src/main/templates/Kproxy/Filter.ftl
diff --git a/kroxylicious/src/main/templates/Kproxy/KrpcFilter.ftl b/api/kroxylicious-filter-api/src/main/templates/Kproxy/KrpcFilter.ftl
similarity index 100%
rename from kroxylicious/src/main/templates/Kproxy/KrpcFilter.ftl
rename to api/kroxylicious-filter-api/src/main/templates/Kproxy/KrpcFilter.ftl
diff --git a/benchmark_output_to_json.sh b/benchmark_output_to_json.sh
new file mode 100755
index 00000000..c8b0b673
--- /dev/null
+++ b/benchmark_output_to_json.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+#
+# Copyright Kroxylicious Authors.
+#
+# Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+#
+
+DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
+ENTRY_FILE=$1
+OUTPUT_FILE=${2:-output.json}
+STATS=$(tail -n 1 "$ENTRY_FILE")
+
+# STATS format: 10000000 records sent, 62987.761478 records/sec (61.51 MB/sec), 13.64 ms avg latency, 1301.00 ms max latency, 0 ms 50th, 27 ms 95th, 332 ms 99th, 1285 ms 99.9th.
+
+IFS=',' read -ra LATENCY_STATS <<< "$STATS"
+IFS=' ' read -ra LATENCY_AVG <<< "${LATENCY_STATS[2]}"
+LATENCY_AVG_MS=${LATENCY_AVG[0]}
+IFS=' ' read -ra LATENCY_95TH <<< "${LATENCY_STATS[5]}"
+LATENCY_95TH_MS=${LATENCY_95TH[0]}
+IFS=' ' read -ra LATENCY_99TH <<< "${LATENCY_STATS[6]}"
+LATENCY_99TH_MS=${LATENCY_99TH[0]}
+
+JSON_FILE="[ {\"name\": \"AVG Latency\", \"unit\": \"ms\", \"value\": $LATENCY_AVG_MS},
+{\"name\": \"95th Latency\", \"unit\": \"ms\", \"value\": $LATENCY_95TH_MS},
+{\"name\": \"99th Latency\", \"unit\": \"ms\", \"value\": $LATENCY_99TH_MS}]"
+
+echo "$JSON_FILE" | jq . > "$DIR/$OUTPUT_FILE"
diff --git a/kroxylicious/pom.xml b/kroxylicious/pom.xml
index ef9d370d..655184b1 100644
--- a/kroxylicious/pom.xml
+++ b/kroxylicious/pom.xml
@@ -28,6 +28,16 @@
     <name>Kroxylicious Proxy</name>
 
     <dependencies>
+        <dependency>
+            <groupId>io.kroxylicious</groupId>
+            <artifactId>kroxylicious-api</artifactId>
+            <version>0.0.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>io.kroxylicious</groupId>
+            <artifactId>kroxylicious-filter-api</artifactId>
+            <version>0.0.0-SNAPSHOT</version>
+        </dependency>
         <!-- We depend on the specific Netty dependencies not netty-all to reduce the size of fatjars -->
         <dependency>
             <groupId>io.netty</groupId>
@@ -195,44 +205,6 @@
                     </execution>
                 </executions>
             </plugin>
-            <plugin>
-                <groupId>io.kroxylicious</groupId>
-                <artifactId>kroxylicious-krpc-plugin</artifactId>
-                <executions>
-                    <execution>
-                        <id>generate-filters</id>
-                        <goals>
-                            <goal>generate-single</goal>
-                        </goals>
-                        <phase>generate-sources</phase>
-                        <configuration>
-                            <messageSpecDirectory>${project.build.directory}/message-specs/common/message</messageSpecDirectory>
-                            <messageSpecFilter>*{Request,Response}.json</messageSpecFilter>
-                            <templateDirectory>${project.basedir}/src/main/templates</templateDirectory>
-                            <templateNames>Kproxy/Filter.ftl</templateNames>
-                            <outputFilePattern>${messageSpecName}Filter.java</outputFilePattern>
-                            <outputPackage>io.kroxylicious.proxy.filter</outputPackage>
-                            <outputDirectory>${project.build.directory}/generated-sources/krpc</outputDirectory>
-                        </configuration>
-                    </execution>
-                    <execution>
-                        <id>generate-request-filters</id>
-                        <goals>
-                            <goal>generate-multi</goal>
-                        </goals>
-                        <phase>generate-sources</phase>
-                        <configuration>
-                            <messageSpecDirectory>${project.build.directory}/message-specs/common/message</messageSpecDirectory>
-                            <messageSpecFilter>*{Request,Response}.json</messageSpecFilter>
-                            <templateDirectory>${project.basedir}/src/main/templates</templateDirectory>
-                            <templateNames>Kproxy/KrpcFilter.ftl</templateNames>
-                            <outputFilePattern>${templateName}.java</outputFilePattern>
-                            <outputPackage>io.kroxylicious.proxy.filter</outputPackage>
-                            <outputDirectory>${project.build.directory}/generated-sources/krpc</outputDirectory>
-                        </configuration>
-                    </execution>
-                </executions>
-            </plugin>
         </plugins>
     </build>
 
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/Configuration.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/Configuration.java
index 37bad001..1d75984e 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/Configuration.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/Configuration.java
@@ -13,14 +13,14 @@ import io.kroxylicious.proxy.config.micrometer.MicrometerConfiguration;
 
 public class Configuration {
 
-    private final ProxyConfig proxy;
+    private final DefaultProxyConfig proxy;
 
     private final AdminHttpConfiguration adminHttp;
     private final MicrometerConfiguration micrometer;
     private final Map<String, Cluster> clusters;
     private final List<FilterDefinition> filters;
 
-    public Configuration(ProxyConfig proxy,
+    public Configuration(DefaultProxyConfig proxy,
                          AdminHttpConfiguration adminHttp,
                          MicrometerConfiguration micrometer,
                          Map<String, Cluster> clusters,
@@ -32,7 +32,7 @@ public class Configuration {
         this.filters = filters;
     }
 
-    public ProxyConfig proxy() {
+    public DefaultProxyConfig proxy() {
         return proxy;
     }
 
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/ProxyConfig.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/DefaultProxyConfig.java
similarity index 82%
rename from kroxylicious/src/main/java/io/kroxylicious/proxy/config/ProxyConfig.java
rename to kroxylicious/src/main/java/io/kroxylicious/proxy/config/DefaultProxyConfig.java
index f07e01af..6cfdd4fa 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/ProxyConfig.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/DefaultProxyConfig.java
@@ -7,7 +7,7 @@ package io.kroxylicious.proxy.config;
 
 import java.util.Optional;
 
-public class ProxyConfig {
+public class DefaultProxyConfig implements ProxyConfig {
 
     private final String address;
     private final boolean logNetwork;
@@ -16,7 +16,7 @@ public class ProxyConfig {
     private final Optional<String> keyStoreFile;
     private final Optional<String> keyPassword;
 
-    public ProxyConfig(String address, boolean logNetwork, boolean logFrames, boolean useIoUring, Optional<String> keyStoreFile, Optional<String> keyPassword) {
+    public DefaultProxyConfig(String address, boolean logNetwork, boolean logFrames, boolean useIoUring, Optional<String> keyStoreFile, Optional<String> keyPassword) {
         this.address = address;
         this.logNetwork = logNetwork;
         this.logFrames = logFrames;
@@ -25,6 +25,7 @@ public class ProxyConfig {
         this.keyPassword = keyPassword;
     }
 
+    @Override
     public String address() {
         return address;
     }
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/DefaultFilterContext.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/DefaultFilterContext.java
index e82dce65..969fd748 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/DefaultFilterContext.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/DefaultFilterContext.java
@@ -13,6 +13,7 @@ import org.apache.kafka.common.message.ProduceRequestData;
 import org.apache.kafka.common.message.RequestHeaderData;
 import org.apache.kafka.common.protocol.ApiKeys;
 import org.apache.kafka.common.protocol.ApiMessage;
+import org.apache.kafka.common.utils.ByteBufferOutputStream;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -24,6 +25,7 @@ import io.kroxylicious.proxy.filter.KrpcFilter;
 import io.kroxylicious.proxy.filter.KrpcFilterContext;
 import io.kroxylicious.proxy.frame.DecodedFrame;
 import io.kroxylicious.proxy.future.Promise;
+import io.kroxylicious.proxy.internal.util.ByteBufOutputStream;
 
 /**
  * Implementation of {@link KrpcFilterContext}.
@@ -63,18 +65,16 @@ class DefaultFilterContext implements KrpcFilterContext {
     }
 
     /**
-     * Allocate a buffer with the given {@code initialCapacity}.
-     * The returned buffer will be released automatically
-     * TODO when?
+     * Create a ByteBufferOutputStream of the given capacity.
+     * The backing buffer will be deallocated when the request processing is completed
      * @param initialCapacity The initial capacity of the buffer.
-     * @return The allocated buffer.
+     * @return The allocated ByteBufferOutputStream
      */
     @Override
-    public ByteBuf allocate(int initialCapacity) {
+    public ByteBufferOutputStream createByteBufferOutputStream(int initialCapacity) {
         final ByteBuf buffer = channelContext.alloc().heapBuffer(initialCapacity);
         decodedFrame.add(buffer);
-
-        return buffer;
+        return new ByteBufOutputStream(buffer);
     }
 
     @Override
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/filter/FetchResponseTransformationFilter.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/filter/FetchResponseTransformationFilter.java
index 137b4838..3d81a6b0 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/filter/FetchResponseTransformationFilter.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/filter/FetchResponseTransformationFilter.java
@@ -32,7 +32,7 @@ import io.kroxylicious.proxy.config.BaseConfig;
 import io.kroxylicious.proxy.filter.FetchResponseFilter;
 import io.kroxylicious.proxy.filter.KrpcFilterContext;
 import io.kroxylicious.proxy.future.Future;
-import io.kroxylicious.proxy.internal.util.NettyMemoryRecords;
+import io.kroxylicious.proxy.internal.util.MemoryRecordsHelper;
 
 /**
  * An filter for modifying the key/value/header/topic of {@link ApiKeys#FETCH} responses.
@@ -109,7 +109,7 @@ public class FetchResponseTransformationFilter implements FetchResponseFilter {
         for (FetchableTopicResponse topicData : responseData.responses()) {
             for (PartitionData partitionData : topicData.partitions()) {
                 MemoryRecords records = (MemoryRecords) partitionData.records();
-                MemoryRecordsBuilder newRecords = NettyMemoryRecords.builder(context.allocate(records.sizeInBytes()), CompressionType.NONE,
+                MemoryRecordsBuilder newRecords = MemoryRecordsHelper.builder(context.createByteBufferOutputStream(records.sizeInBytes()), CompressionType.NONE,
                         TimestampType.CREATE_TIME, 0);
 
                 for (MutableRecordBatch batch : records.batches()) {
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/filter/ProduceRequestTransformationFilter.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/filter/ProduceRequestTransformationFilter.java
index 313ae9cd..7864d146 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/filter/ProduceRequestTransformationFilter.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/filter/ProduceRequestTransformationFilter.java
@@ -24,7 +24,7 @@ import org.apache.kafka.common.record.TimestampType;
 import io.kroxylicious.proxy.config.BaseConfig;
 import io.kroxylicious.proxy.filter.KrpcFilterContext;
 import io.kroxylicious.proxy.filter.ProduceRequestFilter;
-import io.kroxylicious.proxy.internal.util.NettyMemoryRecords;
+import io.kroxylicious.proxy.internal.util.MemoryRecordsHelper;
 
 /**
  * An filter for modifying the key/value/header/topic of {@link ApiKeys#PRODUCE} requests.
@@ -79,7 +79,7 @@ public class ProduceRequestTransformationFilter implements ProduceRequestFilter
         req.topicData().forEach(topicData -> {
             for (PartitionProduceData partitionData : topicData.partitionData()) {
                 MemoryRecords records = (MemoryRecords) partitionData.records();
-                MemoryRecordsBuilder newRecords = NettyMemoryRecords.builder(ctx.allocate(records.sizeInBytes()), CompressionType.NONE,
+                MemoryRecordsBuilder newRecords = MemoryRecordsHelper.builder(ctx.createByteBufferOutputStream(records.sizeInBytes()), CompressionType.NONE,
                         TimestampType.CREATE_TIME, 0);
 
                 for (MutableRecordBatch batch : records.batches()) {
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/util/ByteBufOutputStream.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/util/ByteBufOutputStream.java
index eca4e3aa..953472ae 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/util/ByteBufOutputStream.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/util/ByteBufOutputStream.java
@@ -14,7 +14,7 @@ import io.netty.buffer.ByteBuf;
 /**
  * This class has been introduced as a work-around to allow using pooled {@link ByteBuf} instances
  * that are allowed to grow on demand while used on {@link org.apache.kafka.common.record.MemoryRecordsBuilder}
- * to create records (using {@link NettyMemoryRecords} factory methods).<br>
+ * to create records (using {@link MemoryRecordsHelper} factory methods).<br>
  */
 public class ByteBufOutputStream extends ByteBufferOutputStream {
 
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/util/NettyMemoryRecords.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/util/MemoryRecordsHelper.java
similarity index 81%
rename from kroxylicious/src/main/java/io/kroxylicious/proxy/internal/util/NettyMemoryRecords.java
rename to kroxylicious/src/main/java/io/kroxylicious/proxy/internal/util/MemoryRecordsHelper.java
index ca5f104c..95c3ee0c 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/util/NettyMemoryRecords.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/util/MemoryRecordsHelper.java
@@ -9,31 +9,23 @@ import org.apache.kafka.common.record.CompressionType;
 import org.apache.kafka.common.record.MemoryRecordsBuilder;
 import org.apache.kafka.common.record.RecordBatch;
 import org.apache.kafka.common.record.TimestampType;
-
-import io.netty.buffer.ByteBuf;
+import org.apache.kafka.common.utils.ByteBufferOutputStream;
 
 /**
  * This is introduces additional factory builder methods then {@link org.apache.kafka.common.record.MemoryRecords#builder} ones,
  * in order to use {@link ByteBufOutputStream}<br>
  *
  */
-public class NettyMemoryRecords {
+public class MemoryRecordsHelper {
 
-    public static MemoryRecordsBuilder builder(ByteBuf buffer,
+    public static MemoryRecordsBuilder builder(ByteBufferOutputStream stream,
                                                CompressionType compressionType,
                                                TimestampType timestampType,
                                                long baseOffset) {
-        return builder(new ByteBufOutputStream(buffer), RecordBatch.CURRENT_MAGIC_VALUE, compressionType, timestampType, baseOffset);
-    }
-
-    private static MemoryRecordsBuilder builder(ByteBufOutputStream stream,
-                                                CompressionType compressionType,
-                                                TimestampType timestampType,
-                                                long baseOffset) {
         return builder(stream, RecordBatch.CURRENT_MAGIC_VALUE, compressionType, timestampType, baseOffset);
     }
 
-    private static MemoryRecordsBuilder builder(ByteBufOutputStream stream,
+    private static MemoryRecordsBuilder builder(ByteBufferOutputStream stream,
                                                 byte magic,
                                                 CompressionType compressionType,
                                                 TimestampType timestampType,
@@ -46,7 +38,7 @@ public class NettyMemoryRecords {
                 RecordBatch.NO_PARTITION_LEADER_EPOCH);
     }
 
-    private static MemoryRecordsBuilder builder(ByteBufOutputStream stream,
+    private static MemoryRecordsBuilder builder(ByteBufferOutputStream stream,
                                                 byte magic,
                                                 CompressionType compressionType,
                                                 TimestampType timestampType,
@@ -61,7 +53,7 @@ public class NettyMemoryRecords {
                 logAppendTime, producerId, producerEpoch, baseSequence, isTransactional, false, partitionLeaderEpoch);
     }
 
-    private static MemoryRecordsBuilder builder(ByteBufOutputStream stream,
+    private static MemoryRecordsBuilder builder(ByteBufferOutputStream stream,
                                                 byte magic,
                                                 CompressionType compressionType,
                                                 TimestampType timestampType,
diff --git a/krpc-code-gen/pom.xml b/krpc-code-gen/pom.xml
index dfc604a2..7ad6b02b 100644
--- a/krpc-code-gen/pom.xml
+++ b/krpc-code-gen/pom.xml
@@ -49,7 +49,7 @@
         <dependency>
             <groupId>org.apache.maven</groupId>
             <artifactId>maven-plugin-api</artifactId>
-            <version>3.6.3</version>
+            <version>3.9.0</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
@@ -61,7 +61,7 @@
         <dependency>
             <groupId>org.apache.maven</groupId>
             <artifactId>maven-core</artifactId>
-            <version>3.6.3</version>
+            <version>3.9.0</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
diff --git a/pom.xml b/pom.xml
index bc4e0adc..aa430d82 100644
--- a/pom.xml
+++ b/pom.xml
@@ -31,12 +31,13 @@
 
         <debezium.version>1.9.0.Final</debezium.version>
         <freemarker.version>2.3.31</freemarker.version>
-        <jackson.version>2.13.3</jackson.version>
+        <jackson.version>2.14.2</jackson.version>
+        <jackson-databind.version>2.14.2</jackson-databind.version>
         <junit.version>5.8.2</junit.version>
-        <kafka.version>3.3.1</kafka.version>
+        <kafka.version>3.4.0</kafka.version>
         <log4j.version>2.17.2</log4j.version>
         <netty.version>4.1.77.Final</netty.version>
-        <testcontainers.version>1.17.3</testcontainers.version>
+        <testcontainers.version>1.17.6</testcontainers.version>
         <netty.io_uring.version>0.0.14.Final</netty.io_uring.version>
         <netty.epoll.classifier>linux-x86_64</netty.epoll.classifier>
         <netty.io_uring.classifier>linux-x86_64</netty.io_uring.classifier>
@@ -78,7 +79,7 @@
             <dependency>
                 <groupId>com.fasterxml.jackson.core</groupId>
                 <artifactId>jackson-databind</artifactId>
-                <version>${jackson.version}</version>
+                <version>${jackson-databind.version}</version>
             </dependency>
             <dependency>
                 <groupId>com.fasterxml.jackson.dataformat</groupId>
@@ -205,6 +206,8 @@
     </dependencyManagement>
 
     <modules>
+        <module>api/kroxylicious-api</module>
+        <module>api/kroxylicious-filter-api</module>
         <module>krpc-code-gen</module>
         <module>kroxylicious</module>
         <module>integrationtests</module>
@@ -221,15 +224,21 @@
                     <version>4.1</version>
                     <configuration>
                         <!--suppress MavenModelInspection -->
-                        <header>${rootdir}/etc/license.txt</header>
                         <strictCheck>true</strictCheck>
-                        <excludes>
-                            <exclude>LICENSE.txt</exclude>
-                            <exclude>**/.dontdelete</exclude>
-                            <!-- Need to keep original copyright for Vert.x classes -->
-                            <exclude>src/main/java/io/kroxylicious/proxy/future/**</exclude>
-                            <exclude>src/main/java/io/kroxylicious/proxy/internal/future/**</exclude>
-                        </excludes>
+                        <licenseSets>
+                            <licenseSet>
+                                <header>${rootdir}/etc/license.txt</header>
+                                <excludes>
+                                    <exclude>LICENSE.txt</exclude>
+                                    <exclude>**/.dontdelete</exclude>
+                                    <exclude>docs/**</exclude>
+                                    <!-- Need to keep original copyright for Vert.x classes -->
+                                    <exclude>src/main/java/io/kroxylicious/proxy/future/**</exclude>
+                                    <exclude>src/main/java/io/kroxylicious/proxy/internal/future/**</exclude>
+                                    <exclude>**/.dontdelete</exclude>
+                                </excludes>
+                            </licenseSet>
+                        </licenseSets>
                     </configuration>
                 </plugin>
                 <plugin>
@@ -261,7 +270,7 @@
                 <plugin>
                     <groupId>org.apache.maven.plugins</groupId>
                     <artifactId>maven-compiler-plugin</artifactId>
-                    <version>3.10.1</version>
+                    <version>3.11.0</version>
                     <configuration>
                         <parameters>true</parameters>
                     </configuration>
@@ -510,6 +519,35 @@
                 </plugins>
             </build>
         </profile>
+        <profile>
+            <!-- note CI is deliberately defined after qa so that it can replace the formatter config plugin -->
+            <!-- see https://maven.apache.org/guides/introduction/introduction-to-profiles.html#profile-order -->
+            <id>ci</id>
+            <activation>
+                <activeByDefault>false</activeByDefault>
+            </activation>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>net.revelc.code.formatter</groupId>
+                        <artifactId>formatter-maven-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>validate-formatting</id>
+                                <goals>
+                                    <goal>validate</goal>
+                                </goals>
+                                <phase>process-sources</phase>
+                            </execution>
+                            <execution>
+                                <id>format</id>
+                                <phase>none</phase>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
         <profile>
             <id>quick</id>
             <activation>


Test patch:
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/service/BaseContributor.java b/api/kroxylicious-api/src/main/java/io/kroxylicious/proxy/service/BaseContributor.java
similarity index 100%
rename from kroxylicious/src/main/java/io/kroxylicious/proxy/service/BaseContributor.java
rename to api/kroxylicious-api/src/main/java/io/kroxylicious/proxy/service/BaseContributor.java
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/service/Contributor.java b/api/kroxylicious-api/src/main/java/io/kroxylicious/proxy/service/Contributor.java
similarity index 100%
rename from kroxylicious/src/main/java/io/kroxylicious/proxy/service/Contributor.java
rename to api/kroxylicious-api/src/main/java/io/kroxylicious/proxy/service/Contributor.java
diff --git a/api/kroxylicious-api/src/test/java/io/kroxylicious/proxy/service/BaseContributorTest.java b/api/kroxylicious-api/src/test/java/io/kroxylicious/proxy/service/BaseContributorTest.java
new file mode 100644
index 00000000..48719711
--- /dev/null
+++ b/api/kroxylicious-api/src/test/java/io/kroxylicious/proxy/service/BaseContributorTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package io.kroxylicious.proxy.service;
+
+import org.junit.jupiter.api.Test;
+
+import io.kroxylicious.proxy.config.BaseConfig;
+import io.kroxylicious.proxy.config.ProxyConfig;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class BaseContributorTest {
+    static class LongConfig extends BaseConfig {
+        private final Long value = 2L;
+
+    }
+
+    static class AnotherConfig extends BaseConfig {
+    }
+
+    static class LongProxyConfig implements ProxyConfig {
+        @Override
+        public String address() {
+            return "3";
+        }
+    }
+
+    @Test
+    public void testDefaultConfigClass() {
+        BaseContributor.BaseContributorBuilder<Long> builder = BaseContributor.builder();
+        builder.add("one", () -> 1L);
+        BaseContributor<Long> baseContributor = new BaseContributor<>(builder) {
+        };
+        Class<? extends BaseConfig> one = baseContributor.getConfigType("one");
+        assertThat(one).isEqualTo(BaseConfig.class);
+    }
+
+    @Test
+    public void testSupplier() {
+        BaseContributor.BaseContributorBuilder<Long> builder = BaseContributor.builder();
+        builder.add("one", () -> 1L);
+        BaseContributor<Long> baseContributor = new BaseContributor<>(builder) {
+        };
+        Long instance = baseContributor.getInstance("one", new LongProxyConfig(), new BaseConfig());
+        assertThat(instance).isEqualTo(1L);
+    }
+
+    @Test
+    public void testSpecifyingConfigType() {
+        BaseContributor.BaseContributorBuilder<Long> builder = BaseContributor.builder();
+        builder.add("fromBaseConfig", LongConfig.class, baseConfig -> baseConfig.value);
+        BaseContributor<Long> baseContributor = new BaseContributor<>(builder) {
+        };
+        Class<? extends BaseConfig> configType = baseContributor.getConfigType("fromBaseConfig");
+        assertThat(configType).isEqualTo(LongConfig.class);
+    }
+
+    @Test
+    public void testSpecifyingConfigTypeInstance() {
+        BaseContributor.BaseContributorBuilder<Long> builder = BaseContributor.builder();
+        builder.add("fromBaseConfig", LongConfig.class, baseConfig -> baseConfig.value);
+        BaseContributor<Long> baseContributor = new BaseContributor<>(builder) {
+        };
+        Long instance = baseContributor.getInstance("fromBaseConfig", new LongProxyConfig(), new LongConfig());
+        assertThat(instance).isEqualTo(2L);
+    }
+
+    @Test
+    public void testFailsIfConfigNotAssignableToSpecifiedType() {
+        BaseContributor.BaseContributorBuilder<Long> builder = BaseContributor.builder();
+        builder.add("fromBaseConfig", LongConfig.class, baseConfig -> baseConfig.value);
+        BaseContributor<Long> baseContributor = new BaseContributor<>(builder) {
+        };
+        AnotherConfig incompatibleConfig = new AnotherConfig();
+        assertThatThrownBy(() -> {
+            baseContributor.getInstance("fromBaseConfig", new LongProxyConfig(), incompatibleConfig);
+        }).isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    public void testUsingProxyConfig() {
+        BaseContributor.BaseContributorBuilder<Long> builder = BaseContributor.builder();
+        builder.add("fromProxyConfig", LongConfig.class, (proxyConfig, longConfig) -> Long.valueOf(proxyConfig.address()));
+        BaseContributor<Long> baseContributor = new BaseContributor<>(builder) {
+        };
+        Long instance = baseContributor.getInstance("fromProxyConfig", new LongProxyConfig(), new LongConfig());
+        assertThat(instance).isEqualTo(3L);
+    }
+
+}
\ No newline at end of file
diff --git a/integrationtests/pom.xml b/integrationtests/pom.xml
index dea8a8e4..9bb669f3 100644
--- a/integrationtests/pom.xml
+++ b/integrationtests/pom.xml
@@ -105,9 +105,9 @@
                         <!-- https://github.com/containers/podman/issues/7927#issuecomment-731525556 - required for testcontainers/podman support -->
                         <TESTCONTAINERS_RYUK_DISABLED>true</TESTCONTAINERS_RYUK_DISABLED>
                     </environmentVariables>
-                    <systemProperties>
+                    <systemPropertyVariables>
                         <container.logs.dir>${project.build.directory}/container-logs/</container.logs.dir>
-                    </systemProperties>
+                    </systemPropertyVariables>
                     <runOrder>random</runOrder>
                 </configuration>
                 <executions>