Repository: kroxylicious/kroxylicious
Pull request #373 (2023-06-07 13:33:11)

Eliminate test configuration model 
### Type of change

_Select the type of your PR_

- Refactoring

### Description

The code base had two parallel configuration models, one in the project proper and an second one used by the tests.  These were kept in sync by hand. This duplication represented a maintenance overhead.

This change:

* Eliminates the test-only model.
* Fluent API allow test authors to create the configuration is retained (albeit with a few breaking changes _affecting the kroxylicious test author persona only_).  These continues to be auto-generated using Sundrio Builders.
* Fluent API continues to be a test-only API
* Hand rolled builders are provided for the pluggable - service loader - parts of the model (filters, micrometer, cluster network address providers). Unfortunately I could not find a reasonable way to get these builders to play really nicely with Sundrio generated builders.

Fundamental to the approach behind this change was the ability to serialise/deserialise the config model with fidelity. To achieve this it was necessary to tease apart the concept of the config model from the runtime entity model.  In concrete terms, this means `VirtualCluster` config objects are used to create `VirtualCluster` model objects in the same way `FilterDefinitions` are materialised into `Filters`.

### Additional Context

_Why are you making this pull request?_


robobario: So if I read it right the builders and sundr dependencies still live in kroxylicious-test-tools, would it make sense to move them into `kroxylicious`? I'm thinking about if someone wanted to programmatically embed Kroxylicious, it would be nice to have access to fluent builders without needing to depend on test tools.
k-wall: > So if I read it right the builders and sundr dependencies still live in kroxylicious-test-tools

Right. With this PR, it remains a test only API. I didn't want to take the step to making it public API without a conversation amongst the team.  

>, would it make sense to move them into `kroxylicious`? 

Yes, we could do that.  The `ExternalBuildables` annotations just need to change into `Buildable` annotations and be placed on the resources themselves.  I actually tested this out already and verified this would work.

> I'm thinking about if someone wanted to programmatically embed Kroxylicious, it would be nice to have access to fluent builders without needing to depend on test tools.

I had thought about this use case too.

What's making me hesitate about making it public API is the fact that the hand rolled builders don't play seamlessly with the auto-generated ones.

we currently have:

```
.addToFilters(new FilterDefinitionBuilder("foo").withConfig("a","b"build())
```

what I really want:

```
.addNewFilter()
.withType("MyFilter")
.withConfig("a", "b")
.endFilter();  // the builder would have custom smarts to invoke our service loader and reject the case where type had not been provided.
```

Unfortunately, as far as I can see, sundio doesn't let you provide your own builder or substitute your own build method.



Patch:
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/KafkaProxy.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/KafkaProxy.java
index a34ab688..b57e4ede 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/KafkaProxy.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/KafkaProxy.java
@@ -10,6 +10,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -34,17 +35,18 @@ import io.netty.util.concurrent.Future;
 
 import io.kroxylicious.proxy.config.Configuration;
 import io.kroxylicious.proxy.config.MicrometerDefinition;
-import io.kroxylicious.proxy.config.VirtualCluster;
 import io.kroxylicious.proxy.config.admin.AdminHttpConfiguration;
 import io.kroxylicious.proxy.internal.KafkaProxyInitializer;
 import io.kroxylicious.proxy.internal.MeterRegistries;
 import io.kroxylicious.proxy.internal.PortConflictDetector;
 import io.kroxylicious.proxy.internal.admin.AdminHttpInitializer;
+import io.kroxylicious.proxy.internal.clusternetworkaddressconfigprovider.ClusterNetworkAddressConfigProviderContributorManager;
 import io.kroxylicious.proxy.internal.net.DefaultNetworkBindingOperationProcessor;
 import io.kroxylicious.proxy.internal.net.Endpoint;
 import io.kroxylicious.proxy.internal.net.EndpointRegistry;
 import io.kroxylicious.proxy.internal.net.NetworkBindingOperationProcessor;
 import io.kroxylicious.proxy.internal.util.Metrics;
+import io.kroxylicious.proxy.model.VirtualCluster;
 
 public final class KafkaProxy implements AutoCloseable {
 
@@ -72,7 +74,9 @@ public final class KafkaProxy implements AutoCloseable {
 
     public KafkaProxy(Configuration config) {
         this.config = config;
-        this.virtualClusterMap = config.virtualClusters();
+        this.virtualClusterMap = config.virtualClusters().entrySet().stream()
+                .collect(Collectors.toMap(Map.Entry::getKey,
+                        e -> toVirtualClusterModel(e.getValue())));
         this.adminHttpConfig = config.adminHttpConfig();
         this.micrometerConfig = config.getMicrometer();
     }
@@ -162,12 +166,12 @@ public final class KafkaProxy implements AutoCloseable {
                                            MeterRegistries meterRegistries)
             throws InterruptedException {
         if (adminHttpConfig != null
-                && adminHttpConfig.getEndpoints().maybePrometheus().isPresent()) {
+                && adminHttpConfig.endpoints().maybePrometheus().isPresent()) {
             ServerBootstrap metricsBootstrap = new ServerBootstrap().group(eventGroupConfig.bossGroup(), eventGroupConfig.workerGroup())
                     .option(ChannelOption.SO_REUSEADDR, true)
                     .channel(eventGroupConfig.clazz())
                     .childHandler(new AdminHttpInitializer(meterRegistries, adminHttpConfig));
-            metricsChannel = metricsBootstrap.bind(adminHttpConfig.getHost(), adminHttpConfig.getPort()).sync().channel();
+            metricsChannel = metricsBootstrap.bind(adminHttpConfig.host(), adminHttpConfig.port()).sync().channel();
         }
     }
 
@@ -231,4 +235,13 @@ public final class KafkaProxy implements AutoCloseable {
             shutdown();
         }
     }
+
+    private static VirtualCluster toVirtualClusterModel(io.kroxylicious.proxy.config.VirtualCluster configModel) {
+        return new VirtualCluster(configModel.targetCluster(),
+                ClusterNetworkAddressConfigProviderContributorManager.getInstance()
+                        .getClusterEndpointConfigProvider(configModel.clusterNetworkAddressConfigProvider().type(),
+                                configModel.clusterNetworkAddressConfigProvider().config()),
+                configModel.keyStoreFile(), configModel.keyPassword(),
+                configModel.logNetwork(), configModel.logFrames());
+    }
 }
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/AbstractDefinitionBuilder.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/AbstractDefinitionBuilder.java
new file mode 100644
index 00000000..beeab754
--- /dev/null
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/AbstractDefinitionBuilder.java
@@ -0,0 +1,56 @@
+/*
+ * 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.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public abstract class AbstractDefinitionBuilder<D> {
+    protected static final ObjectMapper mapper = ConfigParser.createObjectMapper();
+
+    private final String type;
+    private Map<String, Object> config;
+
+    public AbstractDefinitionBuilder(String type) {
+        Objects.requireNonNull(type);
+        this.type = type;
+    }
+
+    public AbstractDefinitionBuilder<D> withConfig(Map<String, Object> config) {
+        Objects.requireNonNull(config);
+        if (this.config == null) {
+            this.config = new LinkedHashMap<>();
+        }
+        this.config.putAll(config);
+        return this;
+    }
+
+    public AbstractDefinitionBuilder<D> withConfig(String k1, Object v1) {
+        return withConfig(Map.of(k1, v1));
+    }
+
+    public AbstractDefinitionBuilder<D> withConfig(String k1, Object v1, String k2, Object v2) {
+        return withConfig(Map.of(k1, v1, k2, v2));
+    }
+
+    public AbstractDefinitionBuilder<D> withConfig(String k1, Object v1, String k2, Object v2, String k3, Object v3) {
+        return withConfig(Map.of(k1, v1, k2, v2, k3, v3));
+    }
+
+    public AbstractDefinitionBuilder<D> withConfig(String k1, Object v1, String k2, Object v2, String k3, Object v3, String k4, Object v4) {
+        return withConfig(Map.of(k1, v1, k2, v2, k3, v3, k4, v4));
+    }
+
+    public D build() {
+        return buildInternal(type, config);
+    }
+
+    protected abstract D buildInternal(String type, Map<String, Object> config);
+}
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/ClusterNetworkAddressConfigProviderConverter.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/ClusterNetworkAddressConfigProviderConverter.java
deleted file mode 100644
index 992f3e6c..00000000
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/ClusterNetworkAddressConfigProviderConverter.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright Kroxylicious Authors.
- *
- * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
- */
-
-package io.kroxylicious.proxy.config;
-
-import com.fasterxml.jackson.databind.util.StdConverter;
-
-import io.kroxylicious.proxy.internal.clusternetworkaddressconfigprovider.ClusterNetworkAddressConfigProviderContributorManager;
-import io.kroxylicious.proxy.service.ClusterNetworkAddressConfigProvider;
-
-public class ClusterNetworkAddressConfigProviderConverter extends StdConverter<ClusterNetworkAddressConfigProviderDefinition, ClusterNetworkAddressConfigProvider> {
-
-    @Override
-    public ClusterNetworkAddressConfigProvider convert(ClusterNetworkAddressConfigProviderDefinition value) {
-        return ClusterNetworkAddressConfigProviderContributorManager.getInstance()
-                .getClusterEndpointConfigProvider(value.type(), value.config());
-
-    }
-
-}
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/ClusterNetworkAddressConfigProviderDefinition.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/ClusterNetworkAddressConfigProviderDefinition.java
index bcb573cf..45a43f92 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/ClusterNetworkAddressConfigProviderDefinition.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/ClusterNetworkAddressConfigProviderDefinition.java
@@ -5,27 +5,18 @@
  */
 package io.kroxylicious.proxy.config;
 
+import java.util.Objects;
+
 import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
 import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver;
 
-public class ClusterNetworkAddressConfigProviderDefinition {
-
-    private final String type;
-    private final BaseConfig config;
+public record ClusterNetworkAddressConfigProviderDefinition(@JsonProperty(required = true) String type,
+                                                            @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "type") @JsonTypeIdResolver(ClusterNetworkAddressConfigProviderTypeIdResolver.class) BaseConfig config) {
 
     @JsonCreator
-    public ClusterNetworkAddressConfigProviderDefinition(String type,
-                                                         @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "type") @JsonTypeIdResolver(ClusterNetworkAddressConfigProviderTypeIdResolver.class) BaseConfig config) {
-        this.type = type;
-        this.config = config;
-    }
-
-    public String type() {
-        return type;
-    }
-
-    public BaseConfig config() {
-        return config;
+    public ClusterNetworkAddressConfigProviderDefinition {
+        Objects.requireNonNull(type);
     }
 }
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/ConfigParser.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/ConfigParser.java
index aeda8a27..c5872474 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/ConfigParser.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/ConfigParser.java
@@ -9,35 +9,28 @@ import java.io.IOException;
 import java.io.InputStream;
 
 import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
+import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.PropertyAccessor;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.DeserializationFeature;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.cfg.ConstructorDetector;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
 import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
 import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
 import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
 
+import io.kroxylicious.proxy.service.HostPort;
+
 public class ConfigParser {
 
-    private final ObjectMapper mapper;
-
-    public ConfigParser() {
-        this.mapper = new ObjectMapper(new YAMLFactory())
-                .registerModule(new ParameterNamesModule())
-                .registerModule(new Jdk8Module())
-                .setVisibility(PropertyAccessor.ALL, Visibility.NONE)
-                .setVisibility(PropertyAccessor.FIELD, Visibility.ANY)
-                .setVisibility(PropertyAccessor.CREATOR, Visibility.ANY)
-                .setConstructorDetector(ConstructorDetector.USE_PROPERTIES_BASED)
-                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
-                .configure(DeserializationFeature.FAIL_ON_MISSING_EXTERNAL_TYPE_ID_PROPERTY, false);
-    }
+    private static final ObjectMapper MAPPER = createObjectMapper();
 
     public Configuration parseConfiguration(String configuration) {
 
         try {
-            return mapper.readValue(configuration, Configuration.class);
+            return MAPPER.readValue(configuration, Configuration.class);
         }
         catch (JsonProcessingException e) {
             throw new IllegalArgumentException("Couldn't parse configuration", e);
@@ -47,10 +40,33 @@ public class ConfigParser {
     public Configuration parseConfiguration(InputStream configuration) {
 
         try {
-            return mapper.readValue(configuration, Configuration.class);
+            return MAPPER.readValue(configuration, Configuration.class);
         }
         catch (IOException e) {
             throw new IllegalArgumentException("Couldn't parse configuration", e);
         }
     }
+
+    public String toYaml(Configuration configuration) {
+        try {
+            return MAPPER.writeValueAsString(configuration);
+        }
+        catch (JsonProcessingException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static ObjectMapper createObjectMapper() {
+        return new ObjectMapper(new YAMLFactory())
+                .registerModule(new ParameterNamesModule())
+                .registerModule(new Jdk8Module())
+                .registerModule(new SimpleModule().addSerializer(HostPort.class, new ToStringSerializer()))
+                .setVisibility(PropertyAccessor.ALL, Visibility.NONE)
+                .setVisibility(PropertyAccessor.FIELD, Visibility.ANY)
+                .setVisibility(PropertyAccessor.CREATOR, Visibility.ANY)
+                .setConstructorDetector(ConstructorDetector.USE_PROPERTIES_BASED)
+                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+                .configure(DeserializationFeature.FAIL_ON_MISSING_EXTERNAL_TYPE_ID_PROPERTY, false)
+                .setSerializationInclusion(JsonInclude.Include.NON_DEFAULT);
+    }
 }
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 de136f38..d665f864 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/Configuration.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/Configuration.java
@@ -10,55 +10,20 @@ import java.util.Map;
 
 import io.kroxylicious.proxy.config.admin.AdminHttpConfiguration;
 
-public class Configuration {
-
-    private final AdminHttpConfiguration adminHttp;
-    private final Map<String, VirtualCluster> virtualClusters;
-    private final List<FilterDefinition> filters;
-    private final List<MicrometerDefinition> micrometer;
-    private final boolean useIoUring;
-
-    public Configuration(AdminHttpConfiguration adminHttp,
-                         Map<String, VirtualCluster> virtualClusters,
-                         List<FilterDefinition> filters,
-                         List<MicrometerDefinition> micrometer,
-                         boolean useIoUring) {
-        this.adminHttp = adminHttp;
-        this.virtualClusters = virtualClusters;
-        this.filters = filters;
-        this.micrometer = micrometer;
-        this.useIoUring = useIoUring;
-    }
-
+public record Configuration(AdminHttpConfiguration adminHttp,
+                            Map<String, VirtualCluster> virtualClusters,
+                            List<FilterDefinition> filters,
+                            List<MicrometerDefinition> micrometer,
+                            boolean useIoUring) {
     public AdminHttpConfiguration adminHttpConfig() {
-        return adminHttp;
-    }
-
-    public Map<String, VirtualCluster> virtualClusters() {
-        return virtualClusters;
-    }
-
-    public List<FilterDefinition> filters() {
-        return filters;
+        return adminHttp();
     }
 
     public List<MicrometerDefinition> getMicrometer() {
-        return micrometer == null ? List.of() : micrometer;
+        return micrometer() == null ? List.of() : micrometer();
     }
 
     public boolean isUseIoUring() {
-        return useIoUring;
-    }
-
-    @Override
-    public String toString() {
-        final StringBuilder sb = new StringBuilder("Configuration [");
-        sb.append("adminHttp=").append(adminHttp);
-        sb.append(", virtualClusters=").append(virtualClusters);
-        sb.append(", filters=").append(filters);
-        sb.append(", micrometer=").append(micrometer);
-        sb.append(", useIoUring=").append(useIoUring);
-        sb.append(']');
-        return sb.toString();
+        return useIoUring();
     }
 }
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/FilterDefinition.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/FilterDefinition.java
index 814db4d7..ad1c7fd0 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/FilterDefinition.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/FilterDefinition.java
@@ -5,27 +5,17 @@
  */
 package io.kroxylicious.proxy.config;
 
+import java.util.Objects;
+
 import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
 import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver;
 
-public class FilterDefinition {
-
-    private final String type;
-    private final BaseConfig config;
-
+public record FilterDefinition(@JsonProperty(required = true) String type,
+                               @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "type") @JsonTypeIdResolver(FilterConfigTypeIdResolver.class) BaseConfig config) {
     @JsonCreator
-    public FilterDefinition(String type,
-                            @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "type") @JsonTypeIdResolver(FilterConfigTypeIdResolver.class) BaseConfig config) {
-        this.type = type;
-        this.config = config;
-    }
-
-    public String type() {
-        return type;
-    }
-
-    public BaseConfig config() {
-        return config;
+    public FilterDefinition {
+        Objects.requireNonNull(type);
     }
 }
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/MicrometerDefinition.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/MicrometerDefinition.java
index 30985bba..16ad783d 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/MicrometerDefinition.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/MicrometerDefinition.java
@@ -5,27 +5,18 @@
  */
 package io.kroxylicious.proxy.config;
 
+import java.util.Objects;
+
 import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
 import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver;
 
-public class MicrometerDefinition {
-
-    private final String type;
-    private final BaseConfig config;
+public record MicrometerDefinition(@JsonProperty(required = true) String type,
+                                   @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "type") @JsonTypeIdResolver(MicrometerHookConfigTypeIdResolver.class) BaseConfig config) {
 
     @JsonCreator
-    public MicrometerDefinition(String type,
-                                @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "type") @JsonTypeIdResolver(MicrometerHookConfigTypeIdResolver.class) BaseConfig config) {
-        this.type = type;
-        this.config = config;
-    }
-
-    public String type() {
-        return type;
-    }
-
-    public BaseConfig config() {
-        return config;
+    public MicrometerDefinition {
+        Objects.requireNonNull(type);
     }
 }
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/TargetCluster.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/TargetCluster.java
index 6b86a8ae..cc69b546 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/TargetCluster.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/TargetCluster.java
@@ -15,13 +15,7 @@ import io.kroxylicious.proxy.service.HostPort;
 /**
  * Represents the target (upstream) kafka cluster.
  */
-public class TargetCluster {
-
-    private final String bootstrapServers;
-
-    public TargetCluster(@JsonProperty(value = "bootstrap_servers", required = true) String bootstrapServers) {
-        this.bootstrapServers = bootstrapServers;
-    }
+public record TargetCluster(@JsonProperty(value = "bootstrap_servers", required = true)  String bootstrapServers) {
 
     /**
      * A list of host/port pairs to use for establishing the initial connection to the target (upstream) Kafka cluster.
@@ -29,6 +23,7 @@ public class TargetCluster {
      *
      * @return comma separated list of bootstrap servers.
      */
+    @Override
     public String bootstrapServers() {
         return bootstrapServers;
     }
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/VirtualCluster.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/VirtualCluster.java
index 354f3792..b770ebc6 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/VirtualCluster.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/VirtualCluster.java
@@ -5,149 +5,13 @@
  */
 package io.kroxylicious.proxy.config;
 
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.security.KeyStore;
-import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
-import java.security.UnrecoverableKeyException;
-import java.security.cert.CertificateException;
 import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-import javax.net.ssl.KeyManagerFactory;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
-
-import io.netty.handler.ssl.SslContext;
-import io.netty.handler.ssl.SslContextBuilder;
-
-import io.kroxylicious.proxy.service.ClusterNetworkAddressConfigProvider;
-import io.kroxylicious.proxy.service.HostPort;
-
-public class VirtualCluster implements ClusterNetworkAddressConfigProvider {
-
-    private final TargetCluster targetCluster;
-
-    private final Optional<String> keyStoreFile;
-    private final Optional<String> keyPassword;
-
-    private final boolean logNetwork;
-
-    private final boolean logFrames;
-
-    // There appear to be a Jackson defect that is triggered if this field name matches the name of the ctor's parameter causing it to ignore the converter.
-    private final ClusterNetworkAddressConfigProvider provider;
-
-    public VirtualCluster(@JsonProperty(required = true) TargetCluster targetCluster,
-                          @JsonProperty(required = true) @JsonDeserialize(converter = ClusterNetworkAddressConfigProviderConverter.class) ClusterNetworkAddressConfigProvider clusterNetworkAddressConfigProvider,
-                          Optional<String> keyStoreFile,
-                          Optional<String> keyPassword,
-                          boolean logNetwork, boolean logFrames) {
-        if (clusterNetworkAddressConfigProvider.requiresTls() && keyStoreFile.isEmpty()) {
-            throw new IllegalStateException("Cluster endpoint provider requires tls, but this virtual cluster does not define it");
-        }
-        var conflicts = clusterNetworkAddressConfigProvider.getExclusivePorts().stream().filter(p -> clusterNetworkAddressConfigProvider.getSharedPorts().contains(p))
-                .collect(Collectors.toSet());
-        if (!conflicts.isEmpty()) {
-            throw new IllegalStateException(
-                    "The set of exclusive ports described by the cluster endpoint provider must be distinct from those described as shared. Intersection: " + conflicts);
-        }
-        this.targetCluster = targetCluster;
-        this.logNetwork = logNetwork;
-        this.logFrames = logFrames;
-        this.keyStoreFile = keyStoreFile;
-        this.keyPassword = keyPassword;
-        this.provider = clusterNetworkAddressConfigProvider;
-    }
-
-    public TargetCluster targetCluster() {
-        return targetCluster;
-    }
-
-    public ClusterNetworkAddressConfigProvider getClusterNetworkAddressConfigProvider() {
-        return provider;
-    }
-
-    public Optional<String> keyStoreFile() {
-        return keyStoreFile;
-    }
-
-    public Optional<String> keyPassword() {
-        return keyPassword;
-    }
-
-    public boolean isLogNetwork() {
-        return logNetwork;
-    }
-
-    public boolean isLogFrames() {
-        return logFrames;
-    }
-
-    public boolean isUseTls() {
-        return keyStoreFile.isPresent();
-    }
-
-    public Optional<SslContext> buildSslContext() {
-
-        return keyStoreFile.map(ksf -> {
-            try (var is = new FileInputStream(ksf)) {
-                var password = keyPassword.map(String::toCharArray).orElse(null);
-                var keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
-                keyStore.load(is, password);
-                var keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
-                keyManagerFactory.init(keyStore, password);
-                return SslContextBuilder.forServer(keyManagerFactory).build();
-            }
-            catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException e) {
-                throw new RuntimeException(e);
-            }
-        });
-    }
-
-    @Override
-    public String toString() {
-        final StringBuilder sb = new StringBuilder("VirtualCluster [");
-        sb.append("targetCluster=").append(targetCluster);
-        sb.append(", clusterNetworkAddressConfigProvider=").append(provider);
-        sb.append(", keyStoreFile=").append(keyStoreFile);
-        sb.append(", keyPassword=").append(keyPassword);
-        sb.append(", logNetwork=").append(logNetwork);
-        sb.append(", logFrames=").append(logFrames);
-        sb.append(']');
-        return sb.toString();
-    }
-
-    @Override
-    public HostPort getClusterBootstrapAddress() {
-        return provider.getClusterBootstrapAddress();
-    }
-
-    @Override
-    public HostPort getBrokerAddress(int nodeId) throws IllegalArgumentException {
-        return provider.getBrokerAddress(nodeId);
-    }
-
-    @Override
-    public Optional<String> getBindAddress() {
-        return provider.getBindAddress();
-    }
-
-    @Override
-    public boolean requiresTls() {
-        return provider.requiresTls();
-    }
-
-    @Override
-    public Set<Integer> getExclusivePorts() {
-        return provider.getExclusivePorts();
-    }
 
-    @Override
-    public Set<Integer> getSharedPorts() {
-        return provider.getSharedPorts();
-    }
+public record VirtualCluster(TargetCluster targetCluster,
+                             @JsonProperty(required = true) ClusterNetworkAddressConfigProviderDefinition clusterNetworkAddressConfigProvider,
+                             Optional<String> keyStoreFile,
+                             Optional<String> keyPassword,
+                             boolean logNetwork, boolean logFrames) {
 }
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/admin/AdminHttpConfiguration.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/admin/AdminHttpConfiguration.java
index 8c0cc510..34752c21 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/admin/AdminHttpConfiguration.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/admin/AdminHttpConfiguration.java
@@ -5,26 +5,12 @@
  */
 package io.kroxylicious.proxy.config.admin;
 
-public class AdminHttpConfiguration {
-    private final String host;
-    private final Integer port;
-    private final EndpointsConfiguration endpoints;
 
+
+public record AdminHttpConfiguration(String host, Integer port, EndpointsConfiguration endpoints) {
     public AdminHttpConfiguration(String host, Integer port, EndpointsConfiguration endpoints) {
         this.host = host == null ? "0.0.0.0" : host;
         this.port = port == null ? 9193 : port;
         this.endpoints = endpoints == null ? new EndpointsConfiguration(null) : endpoints;
     }
-
-    public EndpointsConfiguration getEndpoints() {
-        return endpoints;
-    }
-
-    public Integer getPort() {
-        return port;
-    }
-
-    public String getHost() {
-        return host;
-    }
 }
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/admin/EndpointsConfiguration.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/admin/EndpointsConfiguration.java
index d3023f2c..78698a99 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/admin/EndpointsConfiguration.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/admin/EndpointsConfiguration.java
@@ -7,13 +7,7 @@ package io.kroxylicious.proxy.config.admin;
 
 import java.util.Optional;
 
-public class EndpointsConfiguration {
-    private final PrometheusMetricsConfig prometheus;
-
-    public EndpointsConfiguration(PrometheusMetricsConfig prometheus) {
-        this.prometheus = prometheus;
-    }
-
+public record EndpointsConfiguration(PrometheusMetricsConfig prometheus) {
     public Optional<PrometheusMetricsConfig> maybePrometheus() {
         return Optional.ofNullable(prometheus);
     }
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/PortConflictDetector.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/PortConflictDetector.java
index 0784e015..3457032a 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/PortConflictDetector.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/PortConflictDetector.java
@@ -15,7 +15,7 @@ import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 
-import io.kroxylicious.proxy.config.VirtualCluster;
+import io.kroxylicious.proxy.model.VirtualCluster;
 
 /**
  * Detects potential for port conflicts arising between virtual cluster configurations.
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/admin/AdminHttpInitializer.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/admin/AdminHttpInitializer.java
index 5df845ea..072ae882 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/admin/AdminHttpInitializer.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/admin/AdminHttpInitializer.java
@@ -30,7 +30,7 @@ public class AdminHttpInitializer extends ChannelInitializer<SocketChannel> {
         p.addLast(new HttpServerCodec());
         p.addLast(new HttpServerExpectContinueHandler());
         RoutingHttpServer.RoutingHttpServerBuilder builder = RoutingHttpServer.builder();
-        adminHttpConfiguration.getEndpoints().maybePrometheus().ifPresent(prometheusMetricsConfig -> {
+        adminHttpConfiguration.endpoints().maybePrometheus().ifPresent(prometheusMetricsConfig -> {
             builder.withRoute(PrometheusMetricsEndpoint.PATH, new PrometheusMetricsEndpoint(registries));
         });
         p.addLast(builder.build());
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/PortPerBrokerClusterNetworkAddressConfigProvider.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/PortPerBrokerClusterNetworkAddressConfigProvider.java
index 4b662a19..a5e15032 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/PortPerBrokerClusterNetworkAddressConfigProvider.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/PortPerBrokerClusterNetworkAddressConfigProvider.java
@@ -129,11 +129,14 @@ public class PortPerBrokerClusterNetworkAddressConfigProvider implements Cluster
             var stripped = matcher.replaceFirst("");
             matcher = ANCHORED_PORT_NUMBER_TOKEN_RE.matcher(stripped);
             if (matcher.find()) {
-                throw new IllegalArgumentException(
-                        "brokerAddressPattern must contain exactly one replacement pattern " + LITERAL_PORT_NUMBER + ". Found too many.");
+                throw new IllegalArgumentException("brokerAddressPattern must contain exactly one replacement pattern " + LITERAL_PORT_NUMBER + ". Found too many.");
             }
 
         }
-    }
 
+        public HostPort getBootstrapAddress() {
+            return bootstrapAddress;
+        }
+
+    }
 }
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/SniRoutingClusterNetworkAddressConfigProvider.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/SniRoutingClusterNetworkAddressConfigProvider.java
index e9a44ece..dbf60397 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/SniRoutingClusterNetworkAddressConfigProvider.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/SniRoutingClusterNetworkAddressConfigProvider.java
@@ -9,6 +9,8 @@ package io.kroxylicious.proxy.internal.clusternetworkaddressconfigprovider;
 import java.util.Set;
 import java.util.regex.Pattern;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
+
 import io.kroxylicious.proxy.config.BaseConfig;
 import io.kroxylicious.proxy.service.ClusterNetworkAddressConfigProvider;
 import io.kroxylicious.proxy.service.HostPort;
@@ -20,7 +22,7 @@ import io.kroxylicious.proxy.service.HostPort;
  */
 public class SniRoutingClusterNetworkAddressConfigProvider implements ClusterNetworkAddressConfigProvider {
 
-    public static final String LITERAL_NODE_ID = "$(nodeId)";
+    private static final String LITERAL_NODE_ID = "$(nodeId)";
     private static final Pattern NODE_ID_TOKEN_RE = Pattern.compile(Pattern.quote(LITERAL_NODE_ID));
     private final HostPort bootstrapAddress;
     private final String brokerAddressPattern;
@@ -66,9 +68,11 @@ public class SniRoutingClusterNetworkAddressConfigProvider implements ClusterNet
     public static class SniRoutingClusterNetworkAddressConfigProviderConfig extends BaseConfig {
 
         private final HostPort bootstrapAddress;
+
         private final String brokerAddressPattern;
 
-        public SniRoutingClusterNetworkAddressConfigProviderConfig(HostPort bootstrapAddress, String brokerAddressPattern) {
+        public SniRoutingClusterNetworkAddressConfigProviderConfig(@JsonProperty(required = true) HostPort bootstrapAddress,
+                                                                   @JsonProperty(required = true) String brokerAddressPattern) {
             if (bootstrapAddress == null) {
                 throw new IllegalArgumentException("bootstrapAddress cannot be null");
             }
@@ -90,5 +94,10 @@ public class SniRoutingClusterNetworkAddressConfigProvider implements ClusterNet
             this.bootstrapAddress = bootstrapAddress;
             this.brokerAddressPattern = brokerAddressPattern;
         }
+
+        public HostPort getBootstrapAddress() {
+            return bootstrapAddress;
+        }
+
     }
 }
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/filter/BrokerAddressFilter.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/filter/BrokerAddressFilter.java
index 2180d154..a5e850f8 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/filter/BrokerAddressFilter.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/filter/BrokerAddressFilter.java
@@ -23,12 +23,12 @@ import org.apache.kafka.common.protocol.ApiMessage;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import io.kroxylicious.proxy.config.VirtualCluster;
 import io.kroxylicious.proxy.filter.DescribeClusterResponseFilter;
 import io.kroxylicious.proxy.filter.FindCoordinatorResponseFilter;
 import io.kroxylicious.proxy.filter.KrpcFilterContext;
 import io.kroxylicious.proxy.filter.MetadataResponseFilter;
 import io.kroxylicious.proxy.internal.net.EndpointReconciler;
+import io.kroxylicious.proxy.model.VirtualCluster;
 import io.kroxylicious.proxy.service.HostPort;
 
 /**
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 8e2b44f5..6309aeeb 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
@@ -21,6 +21,8 @@ import org.apache.kafka.common.record.MutableRecordBatch;
 import org.apache.kafka.common.record.Record;
 import org.apache.kafka.common.record.TimestampType;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
+
 import io.kroxylicious.proxy.config.BaseConfig;
 import io.kroxylicious.proxy.filter.KrpcFilterContext;
 import io.kroxylicious.proxy.filter.ProduceRequestFilter;
@@ -43,7 +45,7 @@ public class ProduceRequestTransformationFilter implements ProduceRequestFilter
 
         private final String transformation;
 
-        public ProduceRequestTransformationConfig(String transformation) {
+        public ProduceRequestTransformationConfig(@JsonProperty(required = true) String transformation) {
             this.transformation = transformation;
         }
 
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/net/EndpointReconciler.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/net/EndpointReconciler.java
index 96755364..2cbaeaae 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/net/EndpointReconciler.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/net/EndpointReconciler.java
@@ -9,7 +9,7 @@ package io.kroxylicious.proxy.internal.net;
 import java.util.Map;
 import java.util.concurrent.CompletionStage;
 
-import io.kroxylicious.proxy.config.VirtualCluster;
+import io.kroxylicious.proxy.model.VirtualCluster;
 import io.kroxylicious.proxy.service.HostPort;
 
 public interface EndpointReconciler {
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/net/EndpointRegistry.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/net/EndpointRegistry.java
index 39853bc4..9bb579fc 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/net/EndpointRegistry.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/net/EndpointRegistry.java
@@ -23,7 +23,7 @@ import org.slf4j.LoggerFactory;
 import io.netty.channel.Channel;
 import io.netty.util.AttributeKey;
 
-import io.kroxylicious.proxy.config.VirtualCluster;
+import io.kroxylicious.proxy.model.VirtualCluster;
 import io.kroxylicious.proxy.service.HostPort;
 
 /**
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/net/VirtualClusterBinding.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/net/VirtualClusterBinding.java
index 742d8a27..077a3e78 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/net/VirtualClusterBinding.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/net/VirtualClusterBinding.java
@@ -6,7 +6,7 @@
 
 package io.kroxylicious.proxy.internal.net;
 
-import io.kroxylicious.proxy.config.VirtualCluster;
+import io.kroxylicious.proxy.model.VirtualCluster;
 import io.kroxylicious.proxy.service.HostPort;
 
 /**
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/net/VirtualClusterBootstrapBinding.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/net/VirtualClusterBootstrapBinding.java
index ac34e4f2..7b909213 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/net/VirtualClusterBootstrapBinding.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/net/VirtualClusterBootstrapBinding.java
@@ -8,7 +8,7 @@ package io.kroxylicious.proxy.internal.net;
 
 import java.util.Objects;
 
-import io.kroxylicious.proxy.config.VirtualCluster;
+import io.kroxylicious.proxy.model.VirtualCluster;
 import io.kroxylicious.proxy.service.HostPort;
 
 /**
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/net/VirtualClusterBrokerBinding.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/net/VirtualClusterBrokerBinding.java
index 6dd83572..04f5423f 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/net/VirtualClusterBrokerBinding.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/net/VirtualClusterBrokerBinding.java
@@ -8,7 +8,7 @@ package io.kroxylicious.proxy.internal.net;
 
 import java.util.Objects;
 
-import io.kroxylicious.proxy.config.VirtualCluster;
+import io.kroxylicious.proxy.model.VirtualCluster;
 import io.kroxylicious.proxy.service.HostPort;
 
 /**
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/model/VirtualCluster.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/model/VirtualCluster.java
new file mode 100644
index 00000000..e5d1a357
--- /dev/null
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/model/VirtualCluster.java
@@ -0,0 +1,150 @@
+/*
+ * 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.model;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.net.ssl.KeyManagerFactory;
+
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+
+import io.kroxylicious.proxy.config.TargetCluster;
+import io.kroxylicious.proxy.service.ClusterNetworkAddressConfigProvider;
+import io.kroxylicious.proxy.service.HostPort;
+
+public class VirtualCluster implements ClusterNetworkAddressConfigProvider {
+
+    private final TargetCluster targetCluster;
+
+    private final Optional<String> keyStoreFile;
+    private final Optional<String> keyPassword;
+
+    private final boolean logNetwork;
+
+    private final boolean logFrames;
+
+    private final ClusterNetworkAddressConfigProvider clusterNetworkAddressConfigProvider;
+
+    public VirtualCluster(TargetCluster targetCluster,
+                          ClusterNetworkAddressConfigProvider clusterNetworkAddressConfigProvider,
+                          Optional<String> keyStoreFile,
+                          Optional<String> keyPassword,
+                          boolean logNetwork, boolean logFrames) {
+        if (clusterNetworkAddressConfigProvider.requiresTls() && keyStoreFile.isEmpty()) {
+            throw new IllegalStateException("Cluster endpoint provider requires tls, but this virtual cluster does not define it");
+        }
+        var conflicts = clusterNetworkAddressConfigProvider.getExclusivePorts().stream().filter(p -> clusterNetworkAddressConfigProvider.getSharedPorts().contains(p))
+                .collect(Collectors.toSet());
+        if (!conflicts.isEmpty()) {
+            throw new IllegalStateException(
+                    "The set of exclusive ports described by the cluster endpoint provider must be distinct from those described as shared. Intersection: " + conflicts);
+        }
+        this.targetCluster = targetCluster;
+        this.logNetwork = logNetwork;
+        this.logFrames = logFrames;
+        this.keyStoreFile = keyStoreFile;
+        this.keyPassword = keyPassword;
+        this.clusterNetworkAddressConfigProvider = clusterNetworkAddressConfigProvider;
+    }
+
+    public TargetCluster targetCluster() {
+        return targetCluster;
+    }
+
+    public ClusterNetworkAddressConfigProvider getClusterNetworkAddressConfigProvider() {
+        return clusterNetworkAddressConfigProvider;
+    }
+
+    public Optional<String> keyStoreFile() {
+        return keyStoreFile;
+    }
+
+    public Optional<String> keyPassword() {
+        return keyPassword;
+    }
+
+    public boolean isLogNetwork() {
+        return logNetwork;
+    }
+
+    public boolean isLogFrames() {
+        return logFrames;
+    }
+
+    public boolean isUseTls() {
+        return keyStoreFile.isPresent();
+    }
+
+    public Optional<SslContext> buildSslContext() {
+
+        return keyStoreFile.map(ksf -> {
+            try (var is = new FileInputStream(ksf)) {
+                var password = keyPassword.map(String::toCharArray).orElse(null);
+                var keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+                keyStore.load(is, password);
+                var keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+                keyManagerFactory.init(keyStore, password);
+                return SslContextBuilder.forServer(keyManagerFactory).build();
+            }
+            catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException e) {
+                throw new RuntimeException(e);
+            }
+        });
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("VirtualCluster [");
+        sb.append("targetCluster=").append(targetCluster);
+        sb.append(", clusterNetworkAddressConfigProvider=").append(clusterNetworkAddressConfigProvider);
+        sb.append(", keyStoreFile=").append(keyStoreFile);
+        sb.append(", keyPassword=").append(keyPassword);
+        sb.append(", logNetwork=").append(logNetwork);
+        sb.append(", logFrames=").append(logFrames);
+        sb.append(']');
+        return sb.toString();
+    }
+
+    @Override
+    public HostPort getClusterBootstrapAddress() {
+        return clusterNetworkAddressConfigProvider.getClusterBootstrapAddress();
+    }
+
+    @Override
+    public HostPort getBrokerAddress(int nodeId) throws IllegalArgumentException {
+        return clusterNetworkAddressConfigProvider.getBrokerAddress(nodeId);
+    }
+
+    @Override
+    public Optional<String> getBindAddress() {
+        return clusterNetworkAddressConfigProvider.getBindAddress();
+    }
+
+    @Override
+    public boolean requiresTls() {
+        return clusterNetworkAddressConfigProvider.requiresTls();
+    }
+
+    @Override
+    public Set<Integer> getExclusivePorts() {
+        return clusterNetworkAddressConfigProvider.getExclusivePorts();
+    }
+
+    @Override
+    public Set<Integer> getSharedPorts() {
+        return clusterNetworkAddressConfigProvider.getSharedPorts();
+    }
+}
diff --git a/pom.xml b/pom.xml
index 405a0cc5..40b61a00 100644
--- a/pom.xml
+++ b/pom.xml
@@ -48,7 +48,7 @@
         <slf4j.version>2.0.7</slf4j.version>
         <micrometer.version>1.11.0</micrometer.version>
         <zjsonpatch.version>0.4.14</zjsonpatch.version>
-        <sundr-builder-annotations.version>0.94.0</sundr-builder-annotations.version>
+        <sundr-builder-annotations.version>0.95.0</sundr-builder-annotations.version>
     </properties>
 
     <name>Kroxylicious Parent</name>


Test patch:
diff --git a/integrationtests/pom.xml b/integrationtests/pom.xml
index d3b6b072..337f7029 100644
--- a/integrationtests/pom.xml
+++ b/integrationtests/pom.xml
@@ -79,11 +79,6 @@
             <artifactId>assertj-core</artifactId>
             <scope>compile</scope>
         </dependency>
-        <!-- https://mvnrepository.com/artifact/io.sundr/builder-annotations -->
-        <dependency>
-            <groupId>io.sundr</groupId>
-            <artifactId>builder-annotations</artifactId>
-        </dependency>
     </dependencies>
 
     <build>
diff --git a/integrationtests/src/test/java/io/kroxylicious/proxy/ExpositionIT.java b/integrationtests/src/test/java/io/kroxylicious/proxy/ExpositionIT.java
index 8d81578e..a43f75f2 100644
--- a/integrationtests/src/test/java/io/kroxylicious/proxy/ExpositionIT.java
+++ b/integrationtests/src/test/java/io/kroxylicious/proxy/ExpositionIT.java
@@ -31,6 +31,10 @@ import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.api.io.TempDir;
 
 import io.kroxylicious.net.IntegrationTestInetAddressResolverProvider;
+import io.kroxylicious.proxy.config.ClusterNetworkAddressConfigProviderDefinitionBuilder;
+import io.kroxylicious.proxy.config.ConfigurationBuilder;
+import io.kroxylicious.proxy.config.FilterDefinitionBuilder;
+import io.kroxylicious.proxy.config.VirtualClusterBuilder;
 import io.kroxylicious.proxy.service.HostPort;
 import io.kroxylicious.test.tester.KroxyliciousTester;
 import io.kroxylicious.testing.kafka.api.KafkaCluster;
@@ -72,17 +76,16 @@ public class ExpositionIT {
 
         String bootstrapServers = cluster.getBootstrapServers();
 
-        var builder = KroxyliciousConfig.builder()
+        var builder = new ConfigurationBuilder()
                 .addToVirtualClusters("demo", new VirtualClusterBuilder()
                         .withNewTargetCluster()
                         .withBootstrapServers(bootstrapServers)
                         .endTargetCluster()
-                        .withNewClusterNetworkAddressConfigProvider()
-                        .withType("PortPerBroker")
-                        .withConfig(Map.of("bootstrapAddress", PROXY_ADDRESS.toString()))
-                        .endClusterNetworkAddressConfigProvider()
+                        .withClusterNetworkAddressConfigProvider(
+                                new ClusterNetworkAddressConfigProviderDefinitionBuilder("PortPerBroker").withConfig("bootstrapAddress", PROXY_ADDRESS)
+                                        .build())
                         .build())
-                .addNewFilter().withType("ApiVersions").endFilter();
+                .addToFilters(new FilterDefinitionBuilder("ApiVersions").build());
 
         var demo = builder.getVirtualClusters().get("demo");
         demo = new VirtualClusterBuilder(demo)
@@ -110,8 +113,8 @@ public class ExpositionIT {
     public void exposesTwoClusterOverPlainWithSeparatePorts(KafkaCluster cluster) {
         List<String> clusterProxyAddresses = List.of("localhost:9192", "localhost:9294");
 
-        var builder = KroxyliciousConfig.builder()
-                .addNewFilter().withType("ApiVersions").endFilter();
+        var builder = new ConfigurationBuilder()
+                .addToFilters(new FilterDefinitionBuilder("ApiVersions").build());
 
         var base = new VirtualClusterBuilder()
                 .withNewTargetCluster()
@@ -122,10 +125,9 @@ public class ExpositionIT {
         for (int i = 0; i < clusterProxyAddresses.size(); i++) {
             var bootstrap = clusterProxyAddresses.get(i);
             var virtualCluster = new VirtualClusterBuilder(base)
-                    .withNewClusterNetworkAddressConfigProvider()
-                    .withType("PortPerBroker")
-                    .withConfig(Map.of("bootstrapAddress", bootstrap))
-                    .endClusterNetworkAddressConfigProvider()
+                    .withClusterNetworkAddressConfigProvider(
+                            new ClusterNetworkAddressConfigProviderDefinitionBuilder("PortPerBroker").withConfig("bootstrapAddress", bootstrap)
+                                    .build())
                     .build();
             builder.addToVirtualClusters("cluster" + i, virtualCluster);
 
@@ -148,8 +150,8 @@ public class ExpositionIT {
         var virtualClusterBootstrapPattern = "bootstrap" + virtualClusterCommonNamePattern;
         var virtualClusterBrokerAddressPattern = "broker-$(nodeId)" + virtualClusterCommonNamePattern;
 
-        var builder = KroxyliciousConfig.builder()
-                .addNewFilter().withType("ApiVersions").endFilter();
+        var builder = new ConfigurationBuilder()
+                .addToFilters(new FilterDefinitionBuilder("ApiVersions").build());
 
         var base = new VirtualClusterBuilder()
                 .withNewTargetCluster()
@@ -169,11 +171,10 @@ public class ExpositionIT {
             clientTrust.add(new ClientTrust(clientTrustStore, brokerCertificateGenerator.getPassword()));
 
             var virtualCluster = new VirtualClusterBuilder(base)
-                    .withNewClusterNetworkAddressConfigProvider()
-                    .withType("SniRouting")
-                    .withConfig(Map.of("bootstrapAddress", virtualClusterFQDN + ":9192",
-                            "brokerAddressPattern", virtualClusterBrokerAddressPattern.formatted(i)))
-                    .endClusterNetworkAddressConfigProvider()
+                    .withClusterNetworkAddressConfigProvider(
+                            new ClusterNetworkAddressConfigProviderDefinitionBuilder("SniRouting").withConfig("bootstrapAddress", virtualClusterFQDN + ":9192",
+                                    "brokerAddressPattern", virtualClusterBrokerAddressPattern.formatted(i))
+                                    .build())
                     .withKeyPassword(brokerCertificateGenerator.getPassword())
                     .withKeyStoreFile(brokerCertificateGenerator.getKeyStoreLocation())
                     .withLogNetwork(true)
@@ -200,17 +201,16 @@ public class ExpositionIT {
     @Test
     public void exposesClusterOfTwoBrokers(@BrokerCluster(numBrokers = 2) KafkaCluster cluster) throws Exception {
 
-        var builder = KroxyliciousConfig.builder()
+        var builder = new ConfigurationBuilder()
                 .addToVirtualClusters("demo", new VirtualClusterBuilder()
                         .withNewTargetCluster()
                         .withBootstrapServers(cluster.getBootstrapServers())
                         .endTargetCluster()
-                        .withNewClusterNetworkAddressConfigProvider()
-                        .withType("PortPerBroker")
-                        .withConfig(Map.of("bootstrapAddress", PROXY_ADDRESS.toString()))
-                        .endClusterNetworkAddressConfigProvider()
+                        .withClusterNetworkAddressConfigProvider(
+                                new ClusterNetworkAddressConfigProviderDefinitionBuilder("PortPerBroker").withConfig("bootstrapAddress", PROXY_ADDRESS)
+                                        .build())
                         .build())
-                .addNewFilter().withType("ApiVersions").endFilter();
+                .addToFilters(new FilterDefinitionBuilder("ApiVersions").build());
 
         var brokerEndpoints = Map.of(0, "localhost:" + (PROXY_ADDRESS.port() + 1), 1, "localhost:" + (PROXY_ADDRESS.port() + 2));
 
@@ -229,17 +229,16 @@ public class ExpositionIT {
 
     @Test
     public void exposedClusterAddsBroker(@BrokerCluster() KafkaCluster cluster) throws Exception {
-        var builder = KroxyliciousConfig.builder()
+        var builder = new ConfigurationBuilder()
                 .addToVirtualClusters("demo", new VirtualClusterBuilder()
                         .withNewTargetCluster()
                         .withBootstrapServers(cluster.getBootstrapServers())
                         .endTargetCluster()
-                        .withNewClusterNetworkAddressConfigProvider()
-                        .withType("PortPerBroker")
-                        .withConfig(Map.of("bootstrapAddress", PROXY_ADDRESS.toString()))
-                        .endClusterNetworkAddressConfigProvider()
+                        .withClusterNetworkAddressConfigProvider(
+                                new ClusterNetworkAddressConfigProviderDefinitionBuilder("PortPerBroker").withConfig("bootstrapAddress", PROXY_ADDRESS)
+                                        .build())
                         .build())
-                .addNewFilter().withType("ApiVersions").endFilter();
+                .addToFilters(new FilterDefinitionBuilder("ApiVersions").build());
 
         try (var tester = kroxyliciousTester(builder)) {
 
@@ -263,17 +262,16 @@ public class ExpositionIT {
 
     @Test
     public void exposedClusterRemovesBroker(@BrokerCluster(numBrokers = 2) KafkaCluster cluster) throws Exception {
-        var builder = KroxyliciousConfig.builder()
+        var builder = new ConfigurationBuilder()
                 .addToVirtualClusters("demo", new VirtualClusterBuilder()
                         .withNewTargetCluster()
                         .withBootstrapServers(cluster.getBootstrapServers())
                         .endTargetCluster()
-                        .withNewClusterNetworkAddressConfigProvider()
-                        .withType("PortPerBroker")
-                        .withConfig(Map.of("bootstrapAddress", PROXY_ADDRESS.toString()))
-                        .endClusterNetworkAddressConfigProvider()
+                        .withClusterNetworkAddressConfigProvider(
+                                new ClusterNetworkAddressConfigProviderDefinitionBuilder("PortPerBroker").withConfig("bootstrapAddress", PROXY_ADDRESS)
+                                        .build())
                         .build())
-                .addNewFilter().withType("ApiVersions").endFilter();
+                .addToFilters(new FilterDefinitionBuilder("ApiVersions").build());
 
         try (var tester = kroxyliciousTester(builder)) {
 
diff --git a/integrationtests/src/test/java/io/kroxylicious/proxy/KroxyStandaloneIT.java b/integrationtests/src/test/java/io/kroxylicious/proxy/KroxyStandaloneIT.java
index 18a3e7eb..ce829174 100644
--- a/integrationtests/src/test/java/io/kroxylicious/proxy/KroxyStandaloneIT.java
+++ b/integrationtests/src/test/java/io/kroxylicious/proxy/KroxyStandaloneIT.java
@@ -23,6 +23,8 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.api.io.TempDir;
 
+import io.kroxylicious.proxy.config.ConfigParser;
+import io.kroxylicious.proxy.config.Configuration;
 import io.kroxylicious.proxy.service.HostPort;
 import io.kroxylicious.testing.kafka.api.KafkaCluster;
 import io.kroxylicious.testing.kafka.junit5ext.KafkaClusterExtension;
@@ -71,13 +73,13 @@ public class KroxyStandaloneIT {
         }
     }
 
-    private record SubprocessKroxyliciousFactory(Path tempDir) implements Function<KroxyliciousConfig, AutoCloseable> {
+    private record SubprocessKroxyliciousFactory(Path tempDir) implements Function<Configuration, AutoCloseable> {
 
     @Override
-    public AutoCloseable apply(KroxyliciousConfig config) {
+    public AutoCloseable apply(Configuration config) {
         try {
             Path configPath = tempDir.resolve("config.yaml");
-            Files.writeString(configPath, config.toYaml());
+            Files.writeString(configPath, new ConfigParser().toYaml(config));
             String java = System.getProperty("java.home") + File.separator + "bin" + File.separator + "java";
             String classpath = System.getProperty("java.class.path");
             var processBuilder = new ProcessBuilder(java, "-cp", classpath, "io.kroxylicious.proxy.Kroxylicious", "-c", configPath.toString()).inheritIO();
diff --git a/integrationtests/src/test/java/io/kroxylicious/proxy/KrpcFilterIT.java b/integrationtests/src/test/java/io/kroxylicious/proxy/KrpcFilterIT.java
index 453592c9..6149a57a 100644
--- a/integrationtests/src/test/java/io/kroxylicious/proxy/KrpcFilterIT.java
+++ b/integrationtests/src/test/java/io/kroxylicious/proxy/KrpcFilterIT.java
@@ -25,6 +25,7 @@ import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 
+import io.kroxylicious.proxy.config.FilterDefinitionBuilder;
 import io.kroxylicious.proxy.filter.CreateTopicRejectFilter;
 import io.kroxylicious.proxy.internal.filter.ByteBufferTransformation;
 import io.kroxylicious.proxy.service.HostPort;
@@ -124,8 +125,8 @@ public class KrpcFilterIT {
 
     @Test
     public void requestFiltersCanRespondWithoutProxying(KafkaCluster cluster, Admin admin) throws Exception {
-        var proxyAddress = HostPort.parse("localhost:9192");
-        var config = withDefaultFilters(proxy(cluster)).addNewFilter().withType("CreateTopicRejectFilter").endFilter();
+        var config = withDefaultFilters(proxy(cluster))
+                .addToFilters(new FilterDefinitionBuilder("CreateTopicRejectFilter").build());
 
         try (var tester = kroxyliciousTester(config);
                 var proxyAdmin = tester.admin()) {
@@ -152,8 +153,8 @@ public class KrpcFilterIT {
                 new NewTopic(TOPIC_1, 1, (short) 1),
                 new NewTopic(TOPIC_2, 1, (short) 1))).all().get();
 
-        var config = withDefaultFilters(proxy(cluster)).addNewFilter().withType("ProduceRequestTransformation")
-                .withConfig(Map.of("transformation", TestEncoder.class.getName())).endFilter();
+        var config = withDefaultFilters(proxy(cluster))
+                .addToFilters(new FilterDefinitionBuilder("ProduceRequestTransformation").withConfig("transformation", TestEncoder.class.getName()).build());
 
         try (var tester = kroxyliciousTester(config);
                 var producer = tester.producer(Map.of(CLIENT_ID_CONFIG, "shouldModifyProduceMessage", DELIVERY_TIMEOUT_MS_CONFIG, 3_600_000));
@@ -186,7 +187,7 @@ public class KrpcFilterIT {
                 new NewTopic(TOPIC_2, 1, (short) 1))).all().get();
 
         var config = withDefaultFilters(proxy(cluster))
-                .addNewFilter().withType("FetchResponseTransformation").withConfig(Map.of("transformation", TestDecoder.class.getName())).endFilter();
+                .addToFilters(new FilterDefinitionBuilder("FetchResponseTransformation").withConfig("transformation", TestDecoder.class.getName()).build());
 
         try (var tester = kroxyliciousTester(config);
                 var producer = tester.producer(Serdes.String(), Serdes.ByteArray(),
diff --git a/integrationtests/src/test/java/io/kroxylicious/proxy/MetricsIT.java b/integrationtests/src/test/java/io/kroxylicious/proxy/MetricsIT.java
index 4a461f2f..97fc4ca1 100644
--- a/integrationtests/src/test/java/io/kroxylicious/proxy/MetricsIT.java
+++ b/integrationtests/src/test/java/io/kroxylicious/proxy/MetricsIT.java
@@ -19,6 +19,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
 import io.micrometer.core.instrument.Metrics;
 import io.netty.handler.codec.http.HttpResponseStatus;
 
+import io.kroxylicious.proxy.config.MicrometerDefinitionBuilder;
 import io.kroxylicious.test.tester.AdminHttpClient;
 import io.kroxylicious.testing.kafka.api.KafkaCluster;
 import io.kroxylicious.testing.kafka.junit5ext.KafkaClusterExtension;
@@ -38,10 +39,11 @@ public class MetricsIT {
 
     @Test
     public void shouldOfferPrometheusMetricsScrapeEndpoint(KafkaCluster cluster) {
-        KroxyliciousConfigBuilder config = proxy(cluster)
+        var config = proxy(cluster)
                 .withNewAdminHttp()
                 .withNewEndpoints()
-                .withPrometheusEndpointConfig(Map.of())
+                .withNewPrometheus()
+                .endPrometheus()
                 .endEndpoints()
                 .endAdminHttp();
 
@@ -57,14 +59,12 @@ public class MetricsIT {
 
     @Test
     public void shouldOfferPrometheusMetricsWithNamedBinder(KafkaCluster cluster) {
-        KroxyliciousConfigBuilder config = proxy(cluster)
-                .addToMicrometer(new MicrometerConfigBuilder()
-                        .withType("StandardBinders")
-                        .withConfig(Map.of("binderNames", List.of("JvmGcMetrics")))
-                        .build())
+        var config = proxy(cluster)
+                .addToMicrometer(new MicrometerDefinitionBuilder("StandardBinders").withConfig("binderNames", List.of("JvmGcMetrics")).build())
                 .withNewAdminHttp()
                 .withNewEndpoints()
-                .withPrometheusEndpointConfig(Map.of())
+                .withNewPrometheus()
+                .endPrometheus()
                 .endEndpoints()
                 .endAdminHttp();
 
@@ -76,11 +76,12 @@ public class MetricsIT {
 
     @Test
     public void shouldOfferPrometheusMetricsWithCommonTags(KafkaCluster cluster) {
-        KroxyliciousConfigBuilder config = proxy(cluster)
-                .addNewMicrometer().withType("CommonTags").withConfig(Map.of("commonTags", Map.of("a", "b"))).endMicrometer()
+        var config = proxy(cluster)
+                .addToMicrometer(new MicrometerDefinitionBuilder("CommonTags").withConfig("commonTags", Map.of("a", "b")).build())
                 .withNewAdminHttp()
                 .withNewEndpoints()
-                .withPrometheusEndpointConfig(Map.of())
+                .withNewPrometheus()
+                .endPrometheus()
                 .endEndpoints()
                 .endAdminHttp();
 
diff --git a/integrationtests/src/test/java/io/kroxylicious/proxy/ProxyRpcTest.java b/integrationtests/src/test/java/io/kroxylicious/proxy/ProxyRpcTest.java
index d7206740..b62147b6 100644
--- a/integrationtests/src/test/java/io/kroxylicious/proxy/ProxyRpcTest.java
+++ b/integrationtests/src/test/java/io/kroxylicious/proxy/ProxyRpcTest.java
@@ -22,6 +22,7 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
 
+import io.kroxylicious.proxy.config.FilterDefinitionBuilder;
 import io.kroxylicious.test.ApiMessageSampleGenerator;
 import io.kroxylicious.test.ApiMessageSampleGenerator.ApiAndVersion;
 import io.kroxylicious.test.Request;
@@ -52,8 +53,9 @@ public class ProxyRpcTest {
 
     @BeforeAll
     public static void beforeAll() {
-        mockTester = mockKafkaKroxyliciousTester((mockBootstrap) -> proxy(mockBootstrap).addNewFilter().withType("ApiVersions").endFilter()
-                .addNewFilter().withType("FixedClientId").withConfig(Map.of("clientId", "fixed")).endFilter());
+        mockTester = mockKafkaKroxyliciousTester((mockBootstrap) -> proxy(mockBootstrap)
+                .addToFilters(new FilterDefinitionBuilder("ApiVersions").build())
+                .addToFilters(new FilterDefinitionBuilder("FixedClientId").withConfig("clientId", "fixed").build()));
     }
 
     @BeforeEach
@@ -63,7 +65,9 @@ public class ProxyRpcTest {
 
     @AfterAll
     public static void afterAll() {
-        mockTester.close();
+        if (mockTester != null) {
+            mockTester.close();
+        }
     }
 
     @MethodSource("scenarios")
diff --git a/integrationtests/src/test/java/io/kroxylicious/proxy/multitenant/MultiTenantIT.java b/integrationtests/src/test/java/io/kroxylicious/proxy/multitenant/MultiTenantIT.java
index ee719d09..f5ade1ec 100644
--- a/integrationtests/src/test/java/io/kroxylicious/proxy/multitenant/MultiTenantIT.java
+++ b/integrationtests/src/test/java/io/kroxylicious/proxy/multitenant/MultiTenantIT.java
@@ -68,9 +68,10 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import io.kroxylicious.net.IntegrationTestInetAddressResolverProvider;
-import io.kroxylicious.proxy.KroxyliciousConfig;
-import io.kroxylicious.proxy.KroxyliciousConfigBuilder;
-import io.kroxylicious.proxy.VirtualClusterBuilder;
+import io.kroxylicious.proxy.config.ClusterNetworkAddressConfigProviderDefinitionBuilder;
+import io.kroxylicious.proxy.config.ConfigurationBuilder;
+import io.kroxylicious.proxy.config.FilterDefinitionBuilder;
+import io.kroxylicious.proxy.config.VirtualClusterBuilder;
 import io.kroxylicious.proxy.service.HostPort;
 import io.kroxylicious.test.tester.KroxyliciousTester;
 import io.kroxylicious.testing.kafka.api.KafkaCluster;
@@ -128,7 +129,7 @@ public class MultiTenantIT {
 
     @Test
     public void createAndDeleteTopic(KafkaCluster cluster) throws Exception {
-        KroxyliciousConfigBuilder config = getConfig(cluster);
+        var config = getConfig(cluster);
 
         try (var tester = kroxyliciousTester(config);
                 var admin = tester.admin(TENANT_1_CLUSTER, commonConfig(Map.of()))) {
@@ -158,7 +159,7 @@ public class MultiTenantIT {
 
     @Test
     public void describeTopic(KafkaCluster cluster) throws Exception {
-        KroxyliciousConfigBuilder config = getConfig(cluster);
+        var config = getConfig(cluster);
         try (var tester = kroxyliciousTester(config);
                 var admin = tester.admin(TENANT_1_CLUSTER, commonConfig(Map.of()))) {
             var created = createTopics(admin, List.of(NEW_TOPIC_1));
@@ -183,7 +184,7 @@ public class MultiTenantIT {
 
     @Test
     public void produceOne(KafkaCluster cluster) throws Exception {
-        KroxyliciousConfigBuilder config = getConfig(cluster);
+        var config = getConfig(cluster);
         try (var tester = kroxyliciousTester(config)) {
             createTopics(tester, TENANT_1_CLUSTER, List.of(NEW_TOPIC_1));
             produceAndVerify(tester, TENANT_1_CLUSTER, TOPIC_1, MY_KEY, MY_VALUE);
@@ -192,7 +193,7 @@ public class MultiTenantIT {
 
     @Test
     public void consumeOne(KafkaCluster cluster) throws Exception {
-        KroxyliciousConfigBuilder config = getConfig(cluster);
+        var config = getConfig(cluster);
         try (var tester = kroxyliciousTester(config)) {
             var groupId = testInfo.getDisplayName();
             createTopics(tester, TENANT_1_CLUSTER, List.of(NEW_TOPIC_1));
@@ -203,7 +204,7 @@ public class MultiTenantIT {
 
     @Test
     public void consumeOneAndOffsetCommit(KafkaCluster cluster) throws Exception {
-        KroxyliciousConfigBuilder config = getConfig(cluster);
+        var config = getConfig(cluster);
         try (var tester = kroxyliciousTester(config)) {
             var groupId = testInfo.getDisplayName();
             createTopics(tester, TENANT_1_CLUSTER, List.of(NEW_TOPIC_1));
@@ -216,7 +217,7 @@ public class MultiTenantIT {
 
     @Test
     public void alterOffsetCommit(KafkaCluster cluster) throws Exception {
-        KroxyliciousConfigBuilder config = getConfig(cluster);
+        var config = getConfig(cluster);
         try (var tester = kroxyliciousTester(config);
                 var admin = tester.admin(TENANT_1_CLUSTER, commonConfig(Map.of()))) {
             var groupId = testInfo.getDisplayName();
@@ -234,7 +235,7 @@ public class MultiTenantIT {
 
     @Test
     public void deleteConsumerGroupOffsets(KafkaCluster cluster) throws Exception {
-        KroxyliciousConfigBuilder config = getConfig(cluster);
+        var config = getConfig(cluster);
         try (var tester = kroxyliciousTester(config);
                 var admin = tester.admin(TENANT_1_CLUSTER, commonConfig(Map.of()))) {
             var groupId = testInfo.getDisplayName();
@@ -254,7 +255,7 @@ public class MultiTenantIT {
 
     @Test
     public void tenantTopicIsolation(KafkaCluster cluster) throws Exception {
-        KroxyliciousConfigBuilder config = getConfig(cluster);
+        var config = getConfig(cluster);
         try (var tester = kroxyliciousTester(config)) {
             createTopics(tester, TENANT_1_CLUSTER, List.of(NEW_TOPIC_1));
             createTopics(tester, TENANT_2_CLUSTER, List.of(NEW_TOPIC_2, NEW_TOPIC_3));
@@ -272,7 +273,7 @@ public class MultiTenantIT {
     @ParameterizedTest
     @EnumSource
     public void tenantConsumeWithGroup(ConsumerStyle consumerStyle, KafkaCluster cluster) throws Exception {
-        KroxyliciousConfigBuilder config = getConfig(cluster);
+        var config = getConfig(cluster);
         try (var tester = kroxyliciousTester(config)) {
             createTopics(tester, TENANT_1_CLUSTER, List.of(NEW_TOPIC_1));
             runConsumerInOrderToCreateGroup(tester, TENANT_1_CLUSTER, "Tenant1Group", NEW_TOPIC_1, consumerStyle);
@@ -283,7 +284,7 @@ public class MultiTenantIT {
     @ParameterizedTest
     @EnumSource
     public void tenantGroupIsolation(ConsumerStyle consumerStyle, KafkaCluster cluster) throws Exception {
-        KroxyliciousConfigBuilder config = getConfig(cluster);
+        var config = getConfig(cluster);
         try (var tester = kroxyliciousTester(config)) {
             createTopics(tester, TENANT_1_CLUSTER, List.of(NEW_TOPIC_1));
             runConsumerInOrderToCreateGroup(tester, TENANT_1_CLUSTER, "Tenant1Group", NEW_TOPIC_1, consumerStyle);
@@ -296,7 +297,7 @@ public class MultiTenantIT {
 
     @Test
     public void describeGroup(KafkaCluster cluster) throws Exception {
-        KroxyliciousConfigBuilder config = getConfig(cluster);
+        var config = getConfig(cluster);
         try (var tester = kroxyliciousTester(config)) {
             createTopics(tester, TENANT_1_CLUSTER, List.of(NEW_TOPIC_1));
             runConsumerInOrderToCreateGroup(tester, TENANT_1_CLUSTER, "Tenant1Group", NEW_TOPIC_1, ConsumerStyle.ASSIGN);
@@ -311,7 +312,7 @@ public class MultiTenantIT {
 
     @Test
     public void produceInTransaction(KafkaCluster cluster) throws Exception {
-        KroxyliciousConfigBuilder config = getConfig(cluster);
+        var config = getConfig(cluster);
         try (var tester = kroxyliciousTester(config)) {
             createTopics(tester, TENANT_1_CLUSTER, List.of(NEW_TOPIC_1));
             produceAndVerify(tester, TENANT_1_CLUSTER,
@@ -322,7 +323,7 @@ public class MultiTenantIT {
 
     @Test
     public void produceAndConsumeInTransaction(KafkaCluster cluster) throws Exception {
-        KroxyliciousConfigBuilder config = getConfig(cluster);
+        var config = getConfig(cluster);
         try (var tester = kroxyliciousTester(config)) {
             // put some records in an input topic
             var inputTopic = "input";
@@ -380,7 +381,7 @@ public class MultiTenantIT {
 
     @Test
     public void describeTransaction(KafkaCluster cluster) throws Exception {
-        KroxyliciousConfigBuilder config = getConfig(cluster);
+        var config = getConfig(cluster);
         try (var tester = kroxyliciousTester(config)) {
             try (var admin = tester.admin(TENANT_1_CLUSTER, commonConfig(Map.of()))) {
                 createTopics(tester, TENANT_1_CLUSTER, List.of(NEW_TOPIC_1));
@@ -399,7 +400,7 @@ public class MultiTenantIT {
 
     @Test
     public void tenantTransactionalIdIsolation(KafkaCluster cluster) throws Exception {
-        KroxyliciousConfigBuilder config = getConfig(cluster);
+        var config = getConfig(cluster);
         try (var tester = kroxyliciousTester(config)) {
             createTopics(tester, TENANT_1_CLUSTER, List.of(NEW_TOPIC_1));
             var tenant1TransactionId = "12345";
@@ -577,16 +578,15 @@ public class MultiTenantIT {
         return deleted;
     }
 
-    private KroxyliciousConfigBuilder getConfig(KafkaCluster cluster) {
-        return KroxyliciousConfig.builder()
+    private ConfigurationBuilder getConfig(KafkaCluster cluster) {
+        return new ConfigurationBuilder()
                 .addToVirtualClusters(TENANT_1_CLUSTER, new VirtualClusterBuilder()
                         .withNewTargetCluster()
                         .withBootstrapServers(cluster.getBootstrapServers())
                         .endTargetCluster()
-                        .withNewClusterNetworkAddressConfigProvider()
-                        .withType("PortPerBroker")
-                        .withConfig(Map.of("bootstrapAddress", TENANT_1_PROXY_ADDRESS.toString()))
-                        .endClusterNetworkAddressConfigProvider()
+                        .withClusterNetworkAddressConfigProvider(
+                                new ClusterNetworkAddressConfigProviderDefinitionBuilder("PortPerBroker").withConfig("bootstrapAddress", TENANT_1_PROXY_ADDRESS)
+                                        .build())
                         .withKeyPassword(certificateGenerator.getPassword())
                         .withKeyStoreFile(certificateGenerator.getKeyStoreLocation())
                         .build())
@@ -594,15 +594,14 @@ public class MultiTenantIT {
                         .withNewTargetCluster()
                         .withBootstrapServers(cluster.getBootstrapServers())
                         .endTargetCluster()
-                        .withNewClusterNetworkAddressConfigProvider()
-                        .withType("PortPerBroker")
-                        .withConfig(Map.of("bootstrapAddress", TENANT_2_PROXY_ADDRESS.toString()))
-                        .endClusterNetworkAddressConfigProvider()
+                        .withClusterNetworkAddressConfigProvider(
+                                new ClusterNetworkAddressConfigProviderDefinitionBuilder("PortPerBroker").withConfig("bootstrapAddress", TENANT_2_PROXY_ADDRESS)
+                                        .build())
                         .withKeyPassword(certificateGenerator.getPassword())
                         .withKeyStoreFile(certificateGenerator.getKeyStoreLocation())
                         .build())
-                .addNewFilter().withType("ApiVersions").endFilter()
-                .addNewFilter().withType("MultiTenant").endFilter();
+                .addToFilters(new FilterDefinitionBuilder("ApiVersions").build())
+                .addToFilters(new FilterDefinitionBuilder("MultiTenant").build());
     }
 
     @NotNull
diff --git a/integrationtests/src/test/java/io/kroxylicious/proxy/schema/validation/JsonSyntaxValidationIT.java b/integrationtests/src/test/java/io/kroxylicious/proxy/schema/validation/JsonSyntaxValidationIT.java
index 6f7fee94..79f3bade 100644
--- a/integrationtests/src/test/java/io/kroxylicious/proxy/schema/validation/JsonSyntaxValidationIT.java
+++ b/integrationtests/src/test/java/io/kroxylicious/proxy/schema/validation/JsonSyntaxValidationIT.java
@@ -24,7 +24,7 @@ import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 
-import io.kroxylicious.proxy.KroxyliciousConfigBuilder;
+import io.kroxylicious.proxy.config.FilterDefinitionBuilder;
 import io.kroxylicious.testing.kafka.api.KafkaCluster;
 import io.kroxylicious.testing.kafka.junit5ext.KafkaClusterExtension;
 
@@ -52,10 +52,11 @@ public class JsonSyntaxValidationIT {
         assertEquals(1, cluster.getNumOfBrokers());
         admin.createTopics(List.of(new NewTopic(TOPIC_1, 1, (short) 1))).all().get();
 
-        KroxyliciousConfigBuilder config = withDefaultFilters(proxy(cluster)).addNewFilter().withType("ProduceValidator").withConfig(Map.of("rules",
-                List.of(Map.of("topicNames", List.of(TOPIC_1), "valueRule",
-                        Map.of("allowsNulls", true, "syntacticallyCorrectJson", Map.of("validateObjectKeysUnique", true))))))
-                .endFilter();
+        var config = withDefaultFilters(proxy(cluster))
+                .addToFilters(new FilterDefinitionBuilder("ProduceValidator").withConfig("rules",
+                        List.of(Map.of("topicNames", List.of(TOPIC_1), "valueRule",
+                                Map.of("allowsNulls", true, "syntacticallyCorrectJson", Map.of("validateObjectKeysUnique", true)))))
+                        .build());
         try (var tester = kroxyliciousTester(config);
                 var producer = tester.producer(getProducerConfig(0, 16384))) {
             Future<RecordMetadata> invalid = producer.send(new ProducerRecord<>(TOPIC_1, "my-key", SYNTACTICALLY_INCORRECT_JSON));
@@ -68,11 +69,11 @@ public class JsonSyntaxValidationIT {
         assertEquals(1, cluster.getNumOfBrokers());
         admin.createTopics(List.of(new NewTopic(TOPIC_1, 1, (short) 1), new NewTopic(TOPIC_2, 1, (short) 1))).all().get();
 
-        var config = withDefaultFilters(proxy(cluster)).addNewFilter().withType("ProduceValidator").withConfig(Map.of("rules",
-                List.of(Map.of("topicNames", List.of(TOPIC_1), "valueRule",
-                        Map.of("allowsNulls", true, "syntacticallyCorrectJson", Map.of("validateObjectKeysUnique", true))))))
-                .endFilter();
-
+        var config = withDefaultFilters(proxy(cluster))
+                .addToFilters(new FilterDefinitionBuilder("ProduceValidator").withConfig("rules",
+                        List.of(Map.of("topicNames", List.of(TOPIC_1), "valueRule",
+                                Map.of("allowsNulls", true, "syntacticallyCorrectJson", Map.of("validateObjectKeysUnique", true)))))
+                        .build());
         try (var tester = kroxyliciousTester(config);
                 var producer = tester.producer(getProducerConfig(0, 16384));
                 var consumer = tester.consumer(Map.of(GROUP_ID_CONFIG, "my-group-id", AUTO_OFFSET_RESET_CONFIG, "earliest"))) {
@@ -92,10 +93,11 @@ public class JsonSyntaxValidationIT {
 
         admin.createTopics(List.of(new NewTopic(TOPIC_1, 1, (short) 1), new NewTopic(TOPIC_2, 1, (short) 1))).all().get();
 
-        var config = withDefaultFilters(proxy(cluster)).addNewFilter().withType("ProduceValidator").withConfig(
-                Map.of("forwardPartialRequests", true, "rules", List.of(Map.of("topicNames", List.of(TOPIC_1, TOPIC_2), "valueRule",
-                        Map.of("allowsNulls", true, "syntacticallyCorrectJson", Map.of("validateObjectKeysUnique", true))))))
-                .endFilter();
+        var config = withDefaultFilters(proxy(cluster))
+                .addToFilters(new FilterDefinitionBuilder("ProduceValidator").withConfig("forwardPartialRequests", true, "rules",
+                        List.of(Map.of("topicNames", List.of(TOPIC_1, TOPIC_2), "valueRule",
+                                Map.of("allowsNulls", true, "syntacticallyCorrectJson", Map.of("validateObjectKeysUnique", true)))))
+                        .build());
 
         try (var tester = kroxyliciousTester(config);
                 var producer = tester.producer(Map.of(LINGER_MS_CONFIG, 5000, TRANSACTIONAL_ID_CONFIG, randomUUID().toString()))) {
@@ -117,10 +119,11 @@ public class JsonSyntaxValidationIT {
         admin.createTopics(List.of(new NewTopic(TOPIC_1, 1, (short) 1), new NewTopic(TOPIC_2, 1, (short) 1))).all().get();
 
         boolean forwardPartialRequests = false;
-        var config = withDefaultFilters(proxy(cluster)).addNewFilter().withType("ProduceValidator").withConfig(
-                Map.of("forwardPartialRequests", forwardPartialRequests, "rules", List.of(Map.of("topicNames", List.of(TOPIC_1, TOPIC_2), "valueRule",
-                        Map.of("allowsNulls", true, "syntacticallyCorrectJson", Map.of("validateObjectKeysUnique", true))))))
-                .endFilter();
+        var config = withDefaultFilters(proxy(cluster))
+                .addToFilters(new FilterDefinitionBuilder("ProduceValidator").withConfig("forwardPartialRequests", forwardPartialRequests, "rules",
+                        List.of(Map.of("topicNames", List.of(TOPIC_1, TOPIC_2), "valueRule",
+                                Map.of("allowsNulls", true, "syntacticallyCorrectJson", Map.of("validateObjectKeysUnique", true)))))
+                        .build());
 
         try (var tester = kroxyliciousTester(config);
                 var producer = tester.producer(getProducerConfig(5000, 16384))) {
@@ -138,10 +141,12 @@ public class JsonSyntaxValidationIT {
 
         admin.createTopics(List.of(new NewTopic(TOPIC_1, 1, (short) 1), new NewTopic(TOPIC_2, 1, (short) 1))).all().get();
 
-        var config = withDefaultFilters(proxy(cluster)).addNewFilter().withType("ProduceValidator").withConfig(
-                Map.of("forwardPartialRequests", true, "rules", List.of(Map.of("topicNames", List.of(TOPIC_1, TOPIC_2), "valueRule",
-                        Map.of("allowsNulls", true, "syntacticallyCorrectJson", Map.of("validateObjectKeysUnique", true))))))
-                .endFilter();
+        var config = withDefaultFilters(proxy(cluster))
+                .addToFilters(new FilterDefinitionBuilder("ProduceValidator")
+                        .withConfig("forwardPartialRequests", true,
+                                "rules", List.of(Map.of("topicNames", List.of(TOPIC_1, TOPIC_2), "valueRule",
+                                        Map.of("allowsNulls", true, "syntacticallyCorrectJson", Map.of("validateObjectKeysUnique", true)))))
+                        .build());
 
         try (var tester = kroxyliciousTester(config);
                 var producer = tester.producer(getProducerConfig(5000, 16384));
@@ -166,10 +171,12 @@ public class JsonSyntaxValidationIT {
 
         admin.createTopics(List.of(new NewTopic(TOPIC_1, 2, (short) 1))).all().get();
 
-        var config = withDefaultFilters(proxy(cluster)).addNewFilter().withType("ProduceValidator").withConfig(
-                Map.of("forwardPartialRequests", true, "rules", List.of(Map.of("topicNames", List.of(TOPIC_1), "valueRule",
-                        Map.of("allowsNulls", true, "syntacticallyCorrectJson", Map.of("validateObjectKeysUnique", true))))))
-                .endFilter();
+        var config = withDefaultFilters(proxy(cluster))
+                .addToFilters(new FilterDefinitionBuilder("ProduceValidator")
+                        .withConfig("forwardPartialRequests", true,
+                                "rules", List.of(Map.of("topicNames", List.of(TOPIC_1), "valueRule",
+                                        Map.of("allowsNulls", true, "syntacticallyCorrectJson", Map.of("validateObjectKeysUnique", true)))))
+                        .build());
 
         try (var tester = kroxyliciousTester(config);
                 var producer = tester.producer(getProducerConfig(5000, 16384));
@@ -194,10 +201,11 @@ public class JsonSyntaxValidationIT {
 
         admin.createTopics(List.of(new NewTopic(TOPIC_1, 1, (short) 1))).all().get();
 
-        var config = withDefaultFilters(proxy(cluster)).addNewFilter().withType("ProduceValidator").withConfig(
-                Map.of("forwardPartialRequests", true, "rules", List.of(Map.of("topicNames", List.of(TOPIC_1), "valueRule",
-                        Map.of("allowsNulls", true, "syntacticallyCorrectJson", Map.of("validateObjectKeysUnique", true))))))
-                .endFilter();
+        var config = withDefaultFilters(proxy(cluster))
+                .addToFilters(new FilterDefinitionBuilder("ProduceValidator").withConfig("forwardPartialRequests", true, "rules",
+                        List.of(Map.of("topicNames", List.of(TOPIC_1), "valueRule",
+                                Map.of("allowsNulls", true, "syntacticallyCorrectJson", Map.of("validateObjectKeysUnique", true)))))
+                        .build());
 
         try (var tester = kroxyliciousTester(config);
                 var producer = tester.producer(getProducerConfig(5000, 16384))) {
@@ -218,10 +226,11 @@ public class JsonSyntaxValidationIT {
 
         admin.createTopics(List.of(new NewTopic(TOPIC_1, 1, (short) 1))).all().get();
 
-        var config = withDefaultFilters(proxy(cluster)).addNewFilter().withType("ProduceValidator").withConfig(Map.of("rules",
-                List.of(Map.of("topicNames", List.of(TOPIC_1), "valueRule",
-                        Map.of("allowsNulls", true, "syntacticallyCorrectJson", Map.of("validateObjectKeysUnique", true))))))
-                .endFilter();
+        var config = withDefaultFilters(proxy(cluster))
+                .addToFilters(new FilterDefinitionBuilder("ProduceValidator").withConfig("rules",
+                        List.of(Map.of("topicNames", List.of(TOPIC_1), "valueRule",
+                                Map.of("allowsNulls", true, "syntacticallyCorrectJson", Map.of("validateObjectKeysUnique", true)))))
+                        .build());
 
         try (var tester = kroxyliciousTester(config);
                 var producer = tester.producer(getProducerConfig(0, 16384))) {
diff --git a/kroxylicious-test-tools/pom.xml b/kroxylicious-test-tools/pom.xml
index 32f7314e..351598c4 100644
--- a/kroxylicious-test-tools/pom.xml
+++ b/kroxylicious-test-tools/pom.xml
@@ -73,6 +73,15 @@
             <groupId>com.fasterxml.jackson.dataformat</groupId>
             <artifactId>jackson-dataformat-yaml</artifactId>
         </dependency>
+        <dependency>
+            <groupId>io.sundr</groupId>
+            <artifactId>builder-annotations</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.flipkart.zjsonpatch</groupId>
+            <artifactId>zjsonpatch</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.junit.jupiter</groupId>
             <artifactId>junit-jupiter</artifactId>
@@ -93,10 +102,6 @@
             <artifactId>mockito-core</artifactId>
             <scope>test</scope>
         </dependency>
-        <dependency>
-            <groupId>io.sundr</groupId>
-            <artifactId>builder-annotations</artifactId>
-        </dependency>
     </dependencies>
     <build>
         <plugins>
@@ -160,6 +165,14 @@
                     </execution>
                 </executions>
             </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <configuration>
+                    <!-- disable for packages containing the sundrio generated classes -->
+                    <excludePackageNames>io.kroxylicious.proxy.config:io.kroxylicious.proxy.config.admin</excludePackageNames>
+                </configuration>
+            </plugin>
         </plugins>
     </build>
 </project>
diff --git a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/AdminHttp.java b/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/AdminHttp.java
deleted file mode 100644
index 6a8d4017..00000000
--- a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/AdminHttp.java
+++ /dev/null
@@ -1,13 +0,0 @@
-/*
- * Copyright Kroxylicious Authors.
- *
- * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
- */
-
-package io.kroxylicious.proxy;
-
-import io.sundr.builder.annotations.Buildable;
-
-@Buildable(editableEnabled = false)
-public record AdminHttp(Endpoints endpoints) {
-}
diff --git a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/ClusterNetworkAddressConfigProvider.java b/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/ClusterNetworkAddressConfigProvider.java
deleted file mode 100644
index 1d012438..00000000
--- a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/ClusterNetworkAddressConfigProvider.java
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright Kroxylicious Authors.
- *
- * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
- */
-
-package io.kroxylicious.proxy;
-
-import java.util.Map;
-
-import com.fasterxml.jackson.annotation.JsonInclude;
-
-import io.sundr.builder.annotations.Buildable;
-
-import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY;
-
-@Buildable(editableEnabled = false)
-public record ClusterNetworkAddressConfigProvider(String type, @JsonInclude(NON_EMPTY) Map<String, Object> config) {
-}
diff --git a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/Endpoints.java b/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/Endpoints.java
deleted file mode 100644
index 698c4db1..00000000
--- a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/Endpoints.java
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Copyright Kroxylicious Authors.
- *
- * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
- */
-
-package io.kroxylicious.proxy;
-
-import java.util.Map;
-
-import com.fasterxml.jackson.annotation.JsonGetter;
-import com.fasterxml.jackson.annotation.JsonInclude;
-
-import io.sundr.builder.annotations.Buildable;
-
-import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;
-
-@Buildable(editableEnabled = false)
-public record Endpoints(@JsonGetter("prometheus") @JsonInclude(NON_NULL) Map<String, String> prometheusEndpointConfig) {
-}
diff --git a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/Filter.java b/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/Filter.java
deleted file mode 100644
index b5549c8c..00000000
--- a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/Filter.java
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright Kroxylicious Authors.
- *
- * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
- */
-
-package io.kroxylicious.proxy;
-
-import java.util.Map;
-
-import com.fasterxml.jackson.annotation.JsonInclude;
-
-import io.sundr.builder.annotations.Buildable;
-
-import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY;
-
-@Buildable(editableEnabled = false)
-public record Filter(String type, @JsonInclude(NON_EMPTY) Map<String, Object> config) {
-}
diff --git a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/KroxyliciousConfig.java b/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/KroxyliciousConfig.java
deleted file mode 100644
index d64c5850..00000000
--- a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/KroxyliciousConfig.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright Kroxylicious Authors.
- *
- * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
- */
-package io.kroxylicious.proxy;
-
-import java.util.List;
-import java.util.Map;
-
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
-import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
-
-import io.sundr.builder.annotations.Buildable;
-
-import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;
-
-@Buildable(editableEnabled = false)
-public class KroxyliciousConfig {
-
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(new YAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER));
-
-    private final Map<String, VirtualCluster> virtualClusters;
-    private final List<Filter> filters;
-
-    @JsonInclude(NON_NULL)
-    private final AdminHttp adminHttp;
-
-    @JsonInclude(NON_NULL)
-    private final List<MicrometerConfig> micrometer;
-    private final boolean useIoUring;
-
-    public static KroxyliciousConfigBuilder builder() {
-        return new KroxyliciousConfigBuilder();
-    }
-
-    public KroxyliciousConfig(Map<String, VirtualCluster> virtualClusters, List<Filter> filters, AdminHttp adminHttp, List<MicrometerConfig> micrometer,
-                              boolean useIoUring) {
-        this.virtualClusters = virtualClusters;
-        this.filters = filters;
-        this.adminHttp = adminHttp;
-        this.micrometer = micrometer;
-        this.useIoUring = useIoUring;
-    }
-
-    public String toYaml() {
-        try {
-            return OBJECT_MAPPER.writeValueAsString(this);
-        }
-        catch (JsonProcessingException e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-    public AdminHttp getAdminHttp() {
-        return adminHttp;
-    }
-
-    public Map<String, VirtualCluster> getVirtualClusters() {
-        return virtualClusters;
-    }
-
-    public List<Filter> getFilters() {
-        return filters;
-    }
-
-    public List<MicrometerConfig> getMicrometer() {
-        return micrometer;
-    }
-}
diff --git a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/MicrometerConfig.java b/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/MicrometerConfig.java
deleted file mode 100644
index 5eb1cb9c..00000000
--- a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/MicrometerConfig.java
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright Kroxylicious Authors.
- *
- * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
- */
-
-package io.kroxylicious.proxy;
-
-import java.util.Map;
-
-import com.fasterxml.jackson.annotation.JsonInclude;
-
-import io.sundr.builder.annotations.Buildable;
-
-import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY;
-
-@Buildable(editableEnabled = false)
-public record MicrometerConfig(String type, @JsonInclude(NON_EMPTY) Map<String, Object> config) {
-}
diff --git a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/Proxy.java b/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/Proxy.java
deleted file mode 100644
index f0b4cbe1..00000000
--- a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/Proxy.java
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright Kroxylicious Authors.
- *
- * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
- */
-
-package io.kroxylicious.proxy;
-
-import com.fasterxml.jackson.annotation.JsonInclude;
-
-import io.sundr.builder.annotations.Buildable;
-
-import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;
-
-@Buildable(editableEnabled = false)
-public record Proxy(String address,
-                    @JsonInclude(NON_NULL) String keyStoreFile,
-                    @JsonInclude(NON_NULL) String keyPassword) {
-}
diff --git a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/TargetCluster.java b/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/TargetCluster.java
deleted file mode 100644
index 5bf66410..00000000
--- a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/TargetCluster.java
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * Copyright Kroxylicious Authors.
- *
- * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
- */
-
-package io.kroxylicious.proxy;
-
-import com.fasterxml.jackson.annotation.JsonGetter;
-
-import io.sundr.builder.annotations.Buildable;
-
-@Buildable(editableEnabled = false)
-public record TargetCluster(@JsonGetter("bootstrap_servers") String bootstrapServers) {
-}
diff --git a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/VirtualCluster.java b/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/VirtualCluster.java
deleted file mode 100644
index e1db1452..00000000
--- a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/VirtualCluster.java
+++ /dev/null
@@ -1,13 +0,0 @@
-/*
- * Copyright Kroxylicious Authors.
- *
- * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
- */
-
-package io.kroxylicious.proxy;
-
-import io.sundr.builder.annotations.Buildable;
-
-@Buildable(editableEnabled = false)
-public record VirtualCluster(TargetCluster targetCluster, ClusterNetworkAddressConfigProvider clusterNetworkAddressConfigProvider, String keyStoreFile, String keyPassword, boolean logNetwork, boolean logFrames) {
-}
diff --git a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/config/BuilderConfig.java b/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/config/BuilderConfig.java
new file mode 100644
index 00000000..3354c71e
--- /dev/null
+++ b/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/config/BuilderConfig.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.proxy.config;
+
+import io.sundr.builder.annotations.ExternalBuildables;
+
+/**
+ * This class exists to configure Sundrio so that builders are generated for the configuration model.
+ */
+@ExternalBuildables(editableEnabled = false, generateBuilderPackage = true, builderPackage = BuilderConfig.TARGET_CONFIG_PACKAGE, value = {
+        "io.kroxylicious.proxy.config.Configuration",
+        "io.kroxylicious.proxy.config.TargetCluster",
+        "io.kroxylicious.proxy.config.VirtualCluster",
+        "io.kroxylicious.proxy.config.admin.AdminHttpConfiguration",
+        "io.kroxylicious.proxy.config.admin.EndpointsConfiguration",
+        "io.kroxylicious.proxy.config.admin.PrometheusMetricsConfig" })
+public final class BuilderConfig {
+    public static final String TARGET_CONFIG_PACKAGE = "io.kroxylicious.proxy.config";
+
+    private BuilderConfig() {
+        throw new IllegalStateException();
+    }
+
+}
diff --git a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/config/ClusterNetworkAddressConfigProviderDefinitionBuilder.java b/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/config/ClusterNetworkAddressConfigProviderDefinitionBuilder.java
new file mode 100644
index 00000000..7ee676d5
--- /dev/null
+++ b/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/config/ClusterNetworkAddressConfigProviderDefinitionBuilder.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 java.util.Map;
+
+import io.kroxylicious.proxy.internal.clusternetworkaddressconfigprovider.ClusterNetworkAddressConfigProviderContributorManager;
+
+public class ClusterNetworkAddressConfigProviderDefinitionBuilder extends AbstractDefinitionBuilder<ClusterNetworkAddressConfigProviderDefinition> {
+    public ClusterNetworkAddressConfigProviderDefinitionBuilder(String type) {
+        super(type);
+    }
+
+    @Override
+    protected ClusterNetworkAddressConfigProviderDefinition buildInternal(String type, Map<String, Object> config) {
+        var configType = ClusterNetworkAddressConfigProviderContributorManager.getInstance().getConfigType(type);
+        return new ClusterNetworkAddressConfigProviderDefinition(type, mapper.convertValue(config, configType));
+    }
+}
diff --git a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/config/FilterDefinitionBuilder.java b/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/config/FilterDefinitionBuilder.java
new file mode 100644
index 00000000..49e7034d
--- /dev/null
+++ b/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/config/FilterDefinitionBuilder.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 java.util.Map;
+
+import io.kroxylicious.proxy.internal.filter.FilterContributorManager;
+
+public class FilterDefinitionBuilder extends AbstractDefinitionBuilder<FilterDefinition> {
+    public FilterDefinitionBuilder(String type) {
+        super(type);
+    }
+
+    @Override
+    protected FilterDefinition buildInternal(String type, Map<String, Object> config) {
+        var configType = FilterContributorManager.getInstance().getConfigType(type);
+        return new FilterDefinition(type, mapper.convertValue(config, configType));
+    }
+}
diff --git a/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/config/MicrometerDefinitionBuilder.java b/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/config/MicrometerDefinitionBuilder.java
new file mode 100644
index 00000000..1337d9fb
--- /dev/null
+++ b/kroxylicious-test-tools/src/main/java/io/kroxylicious/proxy/config/MicrometerDefinitionBuilder.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 java.util.Map;
+
+import io.kroxylicious.proxy.micrometer.MicrometerConfigurationHookContributorManager;
+
+public class MicrometerDefinitionBuilder extends AbstractDefinitionBuilder<MicrometerDefinition> {
+    public MicrometerDefinitionBuilder(String type) {
+        super(type);
+    }
+
+    @Override
+    protected MicrometerDefinition buildInternal(String type, Map<String, Object> config) {
+        var configType = MicrometerConfigurationHookContributorManager.getInstance().getConfigType(type);
+        return new MicrometerDefinition(type, mapper.convertValue(config, configType));
+    }
+}
diff --git a/kroxylicious-test-tools/src/main/java/io/kroxylicious/test/tester/DefaultKroxyliciousTester.java b/kroxylicious-test-tools/src/main/java/io/kroxylicious/test/tester/DefaultKroxyliciousTester.java
index 6156fa6a..e54c60c7 100644
--- a/kroxylicious-test-tools/src/main/java/io/kroxylicious/test/tester/DefaultKroxyliciousTester.java
+++ b/kroxylicious-test-tools/src/main/java/io/kroxylicious/test/tester/DefaultKroxyliciousTester.java
@@ -15,20 +15,21 @@ import org.apache.kafka.clients.producer.Producer;
 import org.apache.kafka.common.serialization.Serde;
 
 import io.kroxylicious.proxy.KafkaProxy;
-import io.kroxylicious.proxy.KroxyliciousConfig;
-import io.kroxylicious.proxy.KroxyliciousConfigBuilder;
 import io.kroxylicious.proxy.config.ConfigParser;
+import io.kroxylicious.proxy.config.Configuration;
+import io.kroxylicious.proxy.config.ConfigurationBuilder;
 import io.kroxylicious.test.client.KafkaClient;
 
 public class DefaultKroxyliciousTester implements KroxyliciousTester {
     private final AutoCloseable proxy;
-    private final KroxyliciousConfig kroxyliciousConfig;
+    private final Configuration kroxyliciousConfig;
 
     private final KroxyliciousClients defaultClients;
 
-    DefaultKroxyliciousTester(KroxyliciousConfigBuilder configurationBuilder) {
+    DefaultKroxyliciousTester(ConfigurationBuilder configurationBuilder) {
         this(configurationBuilder, config -> {
-            KafkaProxy kafkaProxy = new KafkaProxy(new ConfigParser().parseConfiguration(config.toYaml()));
+            var configParser = new ConfigParser();
+            KafkaProxy kafkaProxy = new KafkaProxy(config);
             try {
                 kafkaProxy.startup();
             }
@@ -39,12 +40,12 @@ public class DefaultKroxyliciousTester implements KroxyliciousTester {
         });
     }
 
-    DefaultKroxyliciousTester(KroxyliciousConfigBuilder configuration, Function<KroxyliciousConfig, AutoCloseable> kroxyliciousFactory) {
+    DefaultKroxyliciousTester(ConfigurationBuilder configuration, Function<Configuration, AutoCloseable> kroxyliciousFactory) {
         kroxyliciousConfig = configuration.build();
         proxy = kroxyliciousFactory.apply(kroxyliciousConfig);
-        int numVirtualClusters = kroxyliciousConfig.getVirtualClusters().size();
+        int numVirtualClusters = kroxyliciousConfig.virtualClusters().size();
         if (numVirtualClusters == 1) {
-            String onlyCluster = kroxyliciousConfig.getVirtualClusters().keySet().stream().findFirst().orElseThrow();
+            String onlyCluster = kroxyliciousConfig.virtualClusters().keySet().stream().findFirst().orElseThrow();
             defaultClients = new KroxyliciousClients(KroxyliciousConfigUtils.bootstrapServersFor(onlyCluster, kroxyliciousConfig));
         }
         else {
@@ -53,9 +54,9 @@ public class DefaultKroxyliciousTester implements KroxyliciousTester {
     }
 
     private KroxyliciousClients clients() {
-        int numVirtualClusters = kroxyliciousConfig.getVirtualClusters().size();
+        int numVirtualClusters = kroxyliciousConfig.virtualClusters().size();
         if (numVirtualClusters == 1) {
-            String onlyCluster = kroxyliciousConfig.getVirtualClusters().keySet().stream().findFirst().orElseThrow();
+            String onlyCluster = kroxyliciousConfig.virtualClusters().keySet().stream().findFirst().orElseThrow();
             return new KroxyliciousClients(KroxyliciousConfigUtils.bootstrapServersFor(onlyCluster, kroxyliciousConfig));
         }
         else {
diff --git a/kroxylicious-test-tools/src/main/java/io/kroxylicious/test/tester/KroxyliciousConfigUtils.java b/kroxylicious-test-tools/src/main/java/io/kroxylicious/test/tester/KroxyliciousConfigUtils.java
index 4ed454b8..ab7e5dc8 100644
--- a/kroxylicious-test-tools/src/main/java/io/kroxylicious/test/tester/KroxyliciousConfigUtils.java
+++ b/kroxylicious-test-tools/src/main/java/io/kroxylicious/test/tester/KroxyliciousConfigUtils.java
@@ -6,16 +6,17 @@
 
 package io.kroxylicious.test.tester;
 
-import java.util.Map;
-
-import io.kroxylicious.proxy.ClusterNetworkAddressConfigProvider;
-import io.kroxylicious.proxy.KroxyliciousConfig;
-import io.kroxylicious.proxy.KroxyliciousConfigBuilder;
-import io.kroxylicious.proxy.VirtualCluster;
-import io.kroxylicious.proxy.VirtualClusterBuilder;
+import io.kroxylicious.proxy.config.ClusterNetworkAddressConfigProviderDefinitionBuilder;
+import io.kroxylicious.proxy.config.Configuration;
+import io.kroxylicious.proxy.config.ConfigurationBuilder;
+import io.kroxylicious.proxy.config.FilterDefinitionBuilder;
+import io.kroxylicious.proxy.config.VirtualClusterBuilder;
 import io.kroxylicious.proxy.service.HostPort;
 import io.kroxylicious.testing.kafka.api.KafkaCluster;
 
+import static io.kroxylicious.proxy.internal.clusternetworkaddressconfigprovider.PortPerBrokerClusterNetworkAddressConfigProvider.PortPerBrokerClusterNetworkAddressConfigProviderConfig;
+import static io.kroxylicious.proxy.internal.clusternetworkaddressconfigprovider.SniRoutingClusterNetworkAddressConfigProvider.SniRoutingClusterNetworkAddressConfigProviderConfig;
+
 /**
  * Class for utilities related to manipulating KroxyliciousConfig and it's builder.
  */
@@ -30,15 +31,14 @@ public class KroxyliciousConfigUtils {
      * @param clusterBootstrapServers external bootstrap server
      * @return builder
      */
-    public static KroxyliciousConfigBuilder proxy(String clusterBootstrapServers) {
-        return KroxyliciousConfig.builder().addToVirtualClusters(DEFAULT_VIRTUAL_CLUSTER, new VirtualClusterBuilder()
+    public static ConfigurationBuilder proxy(String clusterBootstrapServers) {
+        return new ConfigurationBuilder().addToVirtualClusters(DEFAULT_VIRTUAL_CLUSTER, new VirtualClusterBuilder()
                 .withNewTargetCluster()
                 .withBootstrapServers(clusterBootstrapServers)
                 .endTargetCluster()
-                .withNewClusterNetworkAddressConfigProvider()
-                .withType("PortPerBroker")
-                .withConfig(Map.of("bootstrapAddress", DEFAULT_PROXY_BOOTSTRAP.toString()))
-                .endClusterNetworkAddressConfigProvider()
+                .withClusterNetworkAddressConfigProvider(
+                        new ClusterNetworkAddressConfigProviderDefinitionBuilder("PortPerBroker").withConfig("bootstrapAddress", DEFAULT_PROXY_BOOTSTRAP)
+                                .build())
                 .build());
     }
 
@@ -48,7 +48,7 @@ public class KroxyliciousConfigUtils {
      * @param cluster kafka cluster to proxy
      * @return builder
      */
-    public static KroxyliciousConfigBuilder proxy(KafkaCluster cluster) {
+    public static ConfigurationBuilder proxy(KafkaCluster cluster) {
         return proxy(cluster.getBootstrapServers());
     }
 
@@ -57,8 +57,8 @@ public class KroxyliciousConfigUtils {
      * @param builder builder to add filters to
      * @return builder
      */
-    public static KroxyliciousConfigBuilder withDefaultFilters(KroxyliciousConfigBuilder builder) {
-        return builder.addNewFilter().withType("ApiVersions").endFilter();
+    public static ConfigurationBuilder withDefaultFilters(ConfigurationBuilder builder) {
+        return builder.addToFilters(new FilterDefinitionBuilder("ApiVersions").build());
     }
 
     /**
@@ -69,15 +69,20 @@ public class KroxyliciousConfigUtils {
      * @throws IllegalStateException if we encounter an unknown endpoint config provider type for the virtualcluster
      * @throws IllegalArgumentException if the virtualCluster is not in the kroxylicious config
      */
-    static String bootstrapServersFor(String virtualCluster, KroxyliciousConfig config) {
-        VirtualCluster cluster = config.getVirtualClusters().get(virtualCluster);
+    static String bootstrapServersFor(String virtualCluster, Configuration config) {
+        var cluster = config.virtualClusters().get(virtualCluster);
         if (cluster == null) {
             throw new IllegalArgumentException("virtualCluster " + virtualCluster + " not found in config: " + config);
         }
-        ClusterNetworkAddressConfigProvider provider = cluster.clusterNetworkAddressConfigProvider();
-        if (provider.type().equals("PortPerBroker") || provider.type().equals("SniRouting")) {
-            Object bootstrapAddress = provider.config().get("bootstrapAddress");
-            return (String) bootstrapAddress;
+        var provider = cluster.clusterNetworkAddressConfigProvider();
+        // Need proper way to do this for embedded use-cases. We should have a way to query kroxy for the virtual cluster's
+        // actual bootstrap after the proxy is started. The provider might support dynamic ports (port 0), so querying the
+        // config might not work.
+        if (provider.config() instanceof PortPerBrokerClusterNetworkAddressConfigProviderConfig c) {
+            return c.getBootstrapAddress().toString();
+        }
+        else if (provider.config() instanceof SniRoutingClusterNetworkAddressConfigProviderConfig c) {
+            return c.getBootstrapAddress().toString();
         }
         else {
             throw new IllegalStateException("I don't know how to handle ClusterEndpointConfigProvider type:" + provider.type());
diff --git a/kroxylicious-test-tools/src/main/java/io/kroxylicious/test/tester/KroxyliciousTesters.java b/kroxylicious-test-tools/src/main/java/io/kroxylicious/test/tester/KroxyliciousTesters.java
index 56bd2ff0..776bfc58 100644
--- a/kroxylicious-test-tools/src/main/java/io/kroxylicious/test/tester/KroxyliciousTesters.java
+++ b/kroxylicious-test-tools/src/main/java/io/kroxylicious/test/tester/KroxyliciousTesters.java
@@ -8,8 +8,8 @@ package io.kroxylicious.test.tester;
 
 import java.util.function.Function;
 
-import io.kroxylicious.proxy.KroxyliciousConfig;
-import io.kroxylicious.proxy.KroxyliciousConfigBuilder;
+import io.kroxylicious.proxy.config.Configuration;
+import io.kroxylicious.proxy.config.ConfigurationBuilder;
 import io.kroxylicious.test.server.MockServer;
 
 /**
@@ -24,7 +24,7 @@ public class KroxyliciousTesters {
      * @param builder configuration builder for the kroxylicious instance
      * @return KroxyliciousTester
      */
-    public static KroxyliciousTester kroxyliciousTester(KroxyliciousConfigBuilder builder) {
+    public static KroxyliciousTester kroxyliciousTester(ConfigurationBuilder builder) {
         return new DefaultKroxyliciousTester(builder);
     }
 
@@ -37,7 +37,7 @@ public class KroxyliciousTesters {
      * @param kroxyliciousFactory factory that takes a KroxyliciousConfig and is responsible for starting a Kroxylicious instance for that config
      * @return KroxyliciousTester
      */
-    public static KroxyliciousTester kroxyliciousTester(KroxyliciousConfigBuilder builder, Function<KroxyliciousConfig, AutoCloseable> kroxyliciousFactory) {
+    public static KroxyliciousTester kroxyliciousTester(ConfigurationBuilder builder, Function<Configuration, AutoCloseable> kroxyliciousFactory) {
         return new DefaultKroxyliciousTester(builder, kroxyliciousFactory);
     }
 
@@ -48,7 +48,7 @@ public class KroxyliciousTesters {
      * @param configurationForMockBootstrap a function that takes the mock broker's bootstrap server address and returns a KroxyliciousConfigBuilder used to configure an in-process Kroxylicious
      * @return KroxyliciousTester
      */
-    public static MockServerKroxyliciousTester mockKafkaKroxyliciousTester(Function<String, KroxyliciousConfigBuilder> configurationForMockBootstrap) {
+    public static MockServerKroxyliciousTester mockKafkaKroxyliciousTester(Function<String, ConfigurationBuilder> configurationForMockBootstrap) {
         return new MockServerKroxyliciousTester(MockServer.startOnRandomPort(), configurationForMockBootstrap);
     }
 }
diff --git a/kroxylicious-test-tools/src/main/java/io/kroxylicious/test/tester/MockServerKroxyliciousTester.java b/kroxylicious-test-tools/src/main/java/io/kroxylicious/test/tester/MockServerKroxyliciousTester.java
index 4071efcd..5043d378 100644
--- a/kroxylicious-test-tools/src/main/java/io/kroxylicious/test/tester/MockServerKroxyliciousTester.java
+++ b/kroxylicious-test-tools/src/main/java/io/kroxylicious/test/tester/MockServerKroxyliciousTester.java
@@ -9,7 +9,7 @@ package io.kroxylicious.test.tester;
 import java.util.List;
 import java.util.function.Function;
 
-import io.kroxylicious.proxy.KroxyliciousConfigBuilder;
+import io.kroxylicious.proxy.config.ConfigurationBuilder;
 import io.kroxylicious.test.Request;
 import io.kroxylicious.test.Response;
 import io.kroxylicious.test.server.MockServer;
@@ -25,7 +25,7 @@ public class MockServerKroxyliciousTester extends DefaultKroxyliciousTester {
 
     private final MockServer mockServer;
 
-    MockServerKroxyliciousTester(MockServer mockServer, Function<String, KroxyliciousConfigBuilder> configurationForMockBootstrap) {
+    MockServerKroxyliciousTester(MockServer mockServer, Function<String, ConfigurationBuilder> configurationForMockBootstrap) {
         super(configurationForMockBootstrap.apply("localhost:" + mockServer.port()));
         this.mockServer = mockServer;
     }
diff --git a/kroxylicious-test-tools/src/test/java/io/kroxylicious/proxy/KroxyliciousConfigTest.java b/kroxylicious-test-tools/src/test/java/io/kroxylicious/proxy/KroxyliciousConfigTest.java
deleted file mode 100644
index 5b6ccc30..00000000
--- a/kroxylicious-test-tools/src/test/java/io/kroxylicious/proxy/KroxyliciousConfigTest.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Copyright Kroxylicious Authors.
- *
- * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
- */
-package io.kroxylicious.proxy;
-
-import java.util.Map;
-
-import org.junit.jupiter.api.Test;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.ArrayNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
-
-import static io.kroxylicious.proxy.KroxyliciousConfig.builder;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-public class KroxyliciousConfigTest {
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(new YAMLFactory());
-
-    @Test
-    public void testVirtualClusterConfig() throws Exception {
-        ObjectNode deserializedConfig = serializeAndDeserialize(
-                builder().addToVirtualClusters("demo",
-                        new VirtualClusterBuilder().withNewTargetCluster().withBootstrapServers("localhost:9092").endTargetCluster().build()).build());
-        ObjectNode clusterObj = assertObjectField(deserializedConfig, "virtualClusters");
-        ObjectNode demoObj = assertObjectField(clusterObj, "demo");
-        ObjectNode targetClusterObj = assertObjectField(demoObj, "targetCluster");
-        assertTextField(targetClusterObj, "bootstrap_servers", "localhost:9092");
-    }
-
-    @Test
-    public void testVirtualClusterConfigWithClusterNetworkAddressConfigProvider() throws Exception {
-        ObjectNode deserializedConfig = serializeAndDeserialize(
-                builder().addToVirtualClusters("demo", new VirtualClusterBuilder()
-                        .withNewClusterNetworkAddressConfigProvider()
-                        .withType("providertype")
-                        .withConfig(Map.of("a", "b"))
-                        .endClusterNetworkAddressConfigProvider()
-                        .build()).build());
-        ObjectNode clusterObj = assertObjectField(deserializedConfig, "virtualClusters");
-        ObjectNode demoObj = assertObjectField(clusterObj, "demo");
-        ObjectNode endpointAssignerObj = assertObjectField(demoObj, "clusterNetworkAddressConfigProvider");
-        assertTextField(endpointAssignerObj, "type", "providertype");
-        ObjectNode endpointAssignerConfig = assertObjectField(endpointAssignerObj, "config");
-        assertTextField(endpointAssignerConfig, "a", "b");
-    }
-
-    @Test
-    public void testTypeOnlyFilter() throws Exception {
-        ObjectNode deserializedConfig = serializeAndDeserialize(builder().addNewFilter().withType("FilterType").endFilter().build());
-        ArrayNode filters = assertArrayField(deserializedConfig, "filters", 1);
-        ObjectNode filterObj = assertOnlyElementIsObject(filters);
-        assertTextField(filterObj, "type", "FilterType");
-    }
-
-    @Test
-    public void testFilterWithSingleParam() throws Exception {
-        ObjectNode deserializedConfig = serializeAndDeserialize(builder().addNewFilter().withType("FilterType").withConfig(Map.of("a", "b")).endFilter().build());
-        ArrayNode filters = assertArrayField(deserializedConfig, "filters", 1);
-        ObjectNode filterObj = assertOnlyElementIsObject(filters);
-        assertTextField(filterObj, "type", "FilterType");
-        ObjectNode filterConfig = assertObjectField(filterObj, "config");
-        assertTextField(filterConfig, "a", "b");
-    }
-
-    @Test
-    public void testPrometheusEndpointConfig() throws Exception {
-        ObjectNode deserializedConfig = serializeAndDeserialize(
-                builder().withNewAdminHttp().withNewEndpoints().withPrometheusEndpointConfig(Map.of()).endEndpoints().endAdminHttp().build());
-        ObjectNode adminHttp = assertObjectField(deserializedConfig, "adminHttp");
-        ObjectNode endpoints = assertObjectField(adminHttp, "endpoints");
-        ObjectNode prometheus = assertObjectField(endpoints, "prometheus");
-        assertTrue(prometheus.isEmpty(), "expect prometheus endpoint to have an empty object serialized");
-    }
-
-    private static ObjectNode serializeAndDeserialize(KroxyliciousConfig builder) throws Exception {
-        String config = builder.toYaml();
-        return OBJECT_MAPPER.reader().readValue(config, ObjectNode.class);
-    }
-
-    private static ObjectNode assertObjectField(ObjectNode o, String fieldName) {
-        assertTrue(o.has(fieldName), "config should have a " + fieldName + " field");
-        JsonNode field = o.get(fieldName);
-        assertTrue(field.isObject(), fieldName + " should be an object");
-        return (ObjectNode) field;
-    }
-
-    private static ArrayNode assertArrayField(ObjectNode o, String fieldName, int size) {
-        assertTrue(o.has(fieldName), "config should have a " + fieldName + " field");
-        JsonNode field = o.get(fieldName);
-        assertTrue(field.isArray(), fieldName + " should be an array");
-        ArrayNode array = (ArrayNode) field;
-        assertEquals(size, array.size(), fieldName + " was unexpected size");
-        return array;
-    }
-
-    private static ObjectNode assertOnlyElementIsObject(ArrayNode o) {
-        assertEquals(1, o.size(), "array had more than one element");
-        JsonNode jsonNode = o.get(0);
-        assertTrue(jsonNode.isObject(), "only array element was not an object");
-        return (ObjectNode) jsonNode;
-    }
-
-    private static void assertTextField(ObjectNode object, String fieldName, String expected) {
-        assertTrue(object.has(fieldName), "expect " + fieldName + " to be defined");
-        JsonNode field = object.get(fieldName);
-        assertTrue(field.isTextual(), fieldName + " should be text");
-        assertEquals(expected, field.asText(), "expect " + fieldName + " to be set");
-    }
-
-}
diff --git a/kroxylicious-test-tools/src/test/java/io/kroxylicious/proxy/config/ConfigurationTest.java b/kroxylicious-test-tools/src/test/java/io/kroxylicious/proxy/config/ConfigurationTest.java
new file mode 100644
index 00000000..17a85d49
--- /dev/null
+++ b/kroxylicious-test-tools/src/test/java/io/kroxylicious/proxy/config/ConfigurationTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.stream.Stream;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import com.flipkart.zjsonpatch.JsonDiff;
+
+import io.kroxylicious.proxy.internal.filter.ProduceRequestTransformationFilter;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class ConfigurationTest {
+
+    private static final ObjectMapper MAPPER = new ObjectMapper(new YAMLFactory());
+    private final ConfigParser configParser = new ConfigParser();
+
+    public static Stream<Arguments> yamlDeserializeSerializeFidelity() {
+        return Stream.of(Arguments.of("Top level flags", """
+                useIoUring: true
+                """),
+                Arguments.of("Virtual cluster (PortPerBroker)", """
+                        virtualClusters:
+                          demo1:
+                            targetCluster:
+                              bootstrap_servers: kafka.example:1234
+                            clusterNetworkAddressConfigProvider:
+                              type: PortPerBroker
+                              config:
+                                bootstrapAddress: cluster1:9192
+                                numberOfBrokerPorts: 1
+                                brokerAddressPattern: localhost:$(portNumber)
+                                brokerStartPort: 9193
+                        """),
+                Arguments.of("Virtual cluster (SniRouting)", """
+                        virtualClusters:
+                          demo1:
+                            targetCluster:
+                              bootstrap_servers: kafka.example:1234
+                            clusterNetworkAddressConfigProvider:
+                              type: SniRouting
+                              config:
+                                bootstrapAddress: cluster1:9192
+                                brokerAddressPattern: broker-$(nodeId):$(portNumber)
+                        """),
+                Arguments.of("Filters", """
+                        filters:
+                        - type: ProduceRequestTransformation
+                          config:
+                            transformation: io.kroxylicious.proxy.internal.filter.ProduceRequestTransformationFilter$UpperCasing
+                        """),
+                Arguments.of("Admin", """
+                        adminHttp:
+                          host: 0.0.0.0
+                          port: 9193
+                          endpoints: {}
+                        """),
+                Arguments.of("Micrometer", """
+                        micrometer:
+                        - type: CommonTags
+                          config:
+                            commonTags:
+                              zone: "euc-1a"
+                              owner: "becky"
+                        """));
+    }
+
+    @ParameterizedTest(name = "{0}")
+    @MethodSource
+    public void yamlDeserializeSerializeFidelity(String name, String config) throws Exception {
+
+        var configuration = configParser.parseConfiguration(config);
+        var roundTripped = configParser.toYaml(configuration);
+
+        var originalJsonNode = MAPPER.reader().readValue(config, JsonNode.class);
+        var roundTrippedJsonNode = MAPPER.reader().readValue(roundTripped, JsonNode.class);
+        var diff = JsonDiff.asJson(originalJsonNode, roundTrippedJsonNode);
+        assertThat(diff).isEmpty();
+
+    }
+
+    public static Stream<Arguments> fluentApiConfigYamlFidelity() {
+        return Stream.of(Arguments.of("Top level",
+                new ConfigurationBuilder().withUseIoUring(true).build(),
+                """
+                        useIoUring: true"""),
+
+                Arguments.of("With filter",
+                        new ConfigurationBuilder()
+                                .addToFilters(new FilterDefinitionBuilder("ProduceRequestTransformation")
+                                        .withConfig("transformation", ProduceRequestTransformationFilter.UpperCasing.class.getName()).build())
+                                .build(),
+                        """
+                                filters:
+                                - type: ProduceRequestTransformation
+                                  config:
+                                    transformation: "io.kroxylicious.proxy.internal.filter.ProduceRequestTransformationFilter$UpperCasing"
+                                """),
+                Arguments.of("With Virtual Cluster",
+                        new ConfigurationBuilder()
+                                .addToVirtualClusters("demo", new VirtualClusterBuilder()
+                                        .withNewTargetCluster()
+                                        .withBootstrapServers("kafka.example:1234")
+                                        .endTargetCluster()
+                                        .withClusterNetworkAddressConfigProvider(
+                                                new ClusterNetworkAddressConfigProviderDefinitionBuilder("SniRouting")
+                                                        .withConfig("bootstrapAddress", "cluster1:9192", "brokerAddressPattern", "broker-$(nodeId):$(portNumber)")
+                                                        .build())
+                                        .build())
+                                .build(),
+                        """
+                                virtualClusters:
+                                  demo:
+                                    targetCluster:
+                                      bootstrap_servers: kafka.example:1234
+                                    clusterNetworkAddressConfigProvider:
+                                      type: SniRouting
+                                      config:
+                                        bootstrapAddress: cluster1:9192
+                                        brokerAddressPattern: broker-$(nodeId):$(portNumber)
+                                """));
+    }
+
+    @ParameterizedTest(name = "{0}")
+    @MethodSource
+    public void fluentApiConfigYamlFidelity(String name, Configuration config, String expected) throws Exception {
+        var yaml = configParser.toYaml(config);
+        var actualJson = MAPPER.reader().readValue(yaml, JsonNode.class);
+        var expectedJson = MAPPER.reader().readValue(expected, JsonNode.class);
+        var diff = JsonDiff.asJson(actualJson, expectedJson);
+        assertThat(diff).isEmpty();
+
+    }
+}
diff --git a/kroxylicious-test-tools/src/test/java/io/kroxylicious/test/tester/KroxyliciousTestersTest.java b/kroxylicious-test-tools/src/test/java/io/kroxylicious/test/tester/KroxyliciousTestersTest.java
index a20cd502..fda1b180 100644
--- a/kroxylicious-test-tools/src/test/java/io/kroxylicious/test/tester/KroxyliciousTestersTest.java
+++ b/kroxylicious-test-tools/src/test/java/io/kroxylicious/test/tester/KroxyliciousTestersTest.java
@@ -36,9 +36,9 @@ import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 
-import io.kroxylicious.proxy.KroxyliciousConfig;
-import io.kroxylicious.proxy.KroxyliciousConfigBuilder;
-import io.kroxylicious.proxy.VirtualClusterBuilder;
+import io.kroxylicious.proxy.config.ClusterNetworkAddressConfigProviderDefinitionBuilder;
+import io.kroxylicious.proxy.config.ConfigurationBuilder;
+import io.kroxylicious.proxy.config.VirtualClusterBuilder;
 import io.kroxylicious.test.Request;
 import io.kroxylicious.test.Response;
 import io.kroxylicious.test.client.KafkaClient;
@@ -159,8 +159,8 @@ public class KroxyliciousTestersTest {
     @Test
     public void testIllegalToAskForDefaultClientsWhenVirtualClustersAmbiguous(KafkaCluster cluster) {
         String clusterBootstrapServers = cluster.getBootstrapServers();
-        KroxyliciousConfigBuilder builder = KroxyliciousConfig.builder();
-        KroxyliciousConfigBuilder proxy = addVirtualCluster(clusterBootstrapServers, addVirtualCluster(clusterBootstrapServers, builder, "foo",
+        ConfigurationBuilder builder = new ConfigurationBuilder();
+        ConfigurationBuilder proxy = addVirtualCluster(clusterBootstrapServers, addVirtualCluster(clusterBootstrapServers, builder, "foo",
                 "localhost:9192"), "bar", "localhost:9296");
         try (var tester = kroxyliciousTester(withDefaultFilters(proxy))) {
             assertThrows(AmbiguousVirtualClusterException.class, tester::singleRequestClient);
@@ -178,16 +178,15 @@ public class KroxyliciousTestersTest {
         }
     }
 
-    private static KroxyliciousConfigBuilder addVirtualCluster(String clusterBootstrapServers, KroxyliciousConfigBuilder builder, String clusterName,
-                                                               String defaultProxyBootstrap) {
+    private static ConfigurationBuilder addVirtualCluster(String clusterBootstrapServers, ConfigurationBuilder builder, String clusterName,
+                                                          String defaultProxyBootstrap) {
         return builder.addToVirtualClusters(clusterName, new VirtualClusterBuilder()
                 .withNewTargetCluster()
                 .withBootstrapServers(clusterBootstrapServers)
                 .endTargetCluster()
-                .withNewClusterNetworkAddressConfigProvider()
-                .withType("PortPerBroker")
-                .withConfig(Map.of("bootstrapAddress", defaultProxyBootstrap))
-                .endClusterNetworkAddressConfigProvider()
+                .withClusterNetworkAddressConfigProvider(
+                        new ClusterNetworkAddressConfigProviderDefinitionBuilder("PortPerBroker").withConfig("bootstrapAddress", defaultProxyBootstrap)
+                                .build())
                 .build());
     }
 
@@ -223,4 +222,4 @@ public class KroxyliciousTestersTest {
         assertEquals(1, records.count());
     }
 
-}
\ No newline at end of file
+}
diff --git a/kroxylicious/src/test/java/io/kroxylicious/proxy/KafkaProxyTest.java b/kroxylicious/src/test/java/io/kroxylicious/proxy/KafkaProxyTest.java
index 6f3cb20d..5368a21b 100644
--- a/kroxylicious/src/test/java/io/kroxylicious/proxy/KafkaProxyTest.java
+++ b/kroxylicious/src/test/java/io/kroxylicious/proxy/KafkaProxyTest.java
@@ -110,7 +110,7 @@ class KafkaProxyTest {
     @MethodSource
     public void missingTls(String name, String config, String expectedMessage) throws Exception {
 
-        var illegalArgumentException = assertThrows(IllegalArgumentException.class, () -> {
+        var illegalArgumentException = assertThrows(IllegalStateException.class, () -> {
             try (var kafkaProxy = new KafkaProxy(new ConfigParser().parseConfiguration(config))) {
             }
         });
diff --git a/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/PortConflictDetectorTest.java b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/PortConflictDetectorTest.java
index d80757dc..c92d45b6 100644
--- a/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/PortConflictDetectorTest.java
+++ b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/PortConflictDetectorTest.java
@@ -17,7 +17,7 @@ import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
 import org.mockito.junit.jupiter.MockitoExtension;
 
-import io.kroxylicious.proxy.config.VirtualCluster;
+import io.kroxylicious.proxy.model.VirtualCluster;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertThrows;
diff --git a/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/PortPerBrokerClusterNetworkAddressConfigProviderTest.java b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/PortPerBrokerClusterNetworkAddressConfigProviderTest.java
index 7d911c36..6f212670 100644
--- a/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/PortPerBrokerClusterNetworkAddressConfigProviderTest.java
+++ b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/PortPerBrokerClusterNetworkAddressConfigProviderTest.java
@@ -13,8 +13,7 @@ import org.junit.jupiter.params.provider.CsvSource;
 import org.junit.jupiter.params.provider.EmptySource;
 import org.junit.jupiter.params.provider.ValueSource;
 
-import io.kroxylicious.proxy.internal.clusternetworkaddressconfigprovider.PortPerBrokerClusterNetworkAddressConfigProvider.PortPerBrokerClusterNetworkAddressConfigProviderConfig;
-
+import static io.kroxylicious.proxy.internal.clusternetworkaddressconfigprovider.PortPerBrokerClusterNetworkAddressConfigProvider.PortPerBrokerClusterNetworkAddressConfigProviderConfig;
 import static io.kroxylicious.proxy.service.HostPort.parse;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -25,7 +24,8 @@ class PortPerBrokerClusterNetworkAddressConfigProviderTest {
     @CsvSource({ "0,1", "-1,1", "4,-1", "10,0" })
     void badBrokerPortDefinition(int brokerStartPort, int numberOfBrokerPorts) {
         Assertions.assertThrows(IllegalArgumentException.class, () -> {
-            new PortPerBrokerClusterNetworkAddressConfigProviderConfig(parse("localhost:1235"), "localhost:$(portNumber)", brokerStartPort, numberOfBrokerPorts);
+            new PortPerBrokerClusterNetworkAddressConfigProviderConfig(parse("localhost:1235"),
+                    "localhost:$(portNumber)", brokerStartPort, numberOfBrokerPorts);
         });
     }
 
@@ -33,7 +33,8 @@ class PortPerBrokerClusterNetworkAddressConfigProviderTest {
     @CsvSource({ "1235,1", "1234,3" })
     void bootstrapBrokerAddressCollision(int brokerStartPort, int numberOfBrokerPorts) {
         Assertions.assertThrows(IllegalArgumentException.class, () -> {
-            new PortPerBrokerClusterNetworkAddressConfigProviderConfig(parse("localhost:1235"), "localhost:$(portNumber)", brokerStartPort, numberOfBrokerPorts);
+            new PortPerBrokerClusterNetworkAddressConfigProviderConfig(parse("localhost:1235"),
+                    "localhost:$(portNumber)", brokerStartPort, numberOfBrokerPorts);
         });
     }
 
@@ -42,13 +43,15 @@ class PortPerBrokerClusterNetworkAddressConfigProviderTest {
             "not.at.$(portNumber).end:1234" })
     @EmptySource
     void invalidBrokerPattern(String input) {
-        assertThrows(IllegalArgumentException.class, () -> new PortPerBrokerClusterNetworkAddressConfigProviderConfig(parse("good:1235"), input, 1, 5));
+        assertThrows(IllegalArgumentException.class,
+                () -> new PortPerBrokerClusterNetworkAddressConfigProviderConfig(parse("good:1235"), input, 1, 5));
     }
 
     @Test
     void portsExhausted() {
         var provider = new PortPerBrokerClusterNetworkAddressConfigProvider(
-                new PortPerBrokerClusterNetworkAddressConfigProviderConfig(parse("localhost:1235"), "localhost:$(portNumber)", 1236, 1));
+                new PortPerBrokerClusterNetworkAddressConfigProviderConfig(parse("localhost:1235"),
+                        "localhost:$(portNumber)", 1236, 1));
         assertThat(provider.getBrokerAddress(0)).isEqualTo(parse("localhost:1236"));
         Assertions.assertThrows(IllegalArgumentException.class, () -> {
             provider.getBrokerAddress(1);
@@ -87,7 +90,8 @@ class PortPerBrokerClusterNetworkAddressConfigProviderTest {
     @Test
     void definesExclusiveAndSharedCorrectly() {
         var provider = new PortPerBrokerClusterNetworkAddressConfigProvider(
-                new PortPerBrokerClusterNetworkAddressConfigProviderConfig(parse("localhost:1235"), "localhost:$(portNumber)", 1236, 2));
+                new PortPerBrokerClusterNetworkAddressConfigProviderConfig(parse("localhost:1235"),
+                        "localhost:$(portNumber)", 1236, 2));
         assertThat(provider.getExclusivePorts()).containsExactlyInAnyOrder(1235, 1236, 1237);
         assertThat(provider.getSharedPorts()).isEmpty();
     }
@@ -95,7 +99,8 @@ class PortPerBrokerClusterNetworkAddressConfigProviderTest {
     @Test
     void generatesBrokerAddresses() {
         var provider = new PortPerBrokerClusterNetworkAddressConfigProvider(
-                new PortPerBrokerClusterNetworkAddressConfigProviderConfig(parse("localhost:1235"), "localhost:$(portNumber)", 1236, 3));
+                new PortPerBrokerClusterNetworkAddressConfigProviderConfig(parse("localhost:1235"),
+                        "localhost:$(portNumber)", 1236, 3));
         assertThat(provider.getClusterBootstrapAddress()).isEqualTo(parse("localhost:1235"));
         assertThat(provider.getBrokerAddress(0)).isEqualTo(parse("localhost:1236"));
         assertThat(provider.getBrokerAddress(1)).isEqualTo(parse("localhost:1237"));
@@ -104,7 +109,8 @@ class PortPerBrokerClusterNetworkAddressConfigProviderTest {
     @Test
     void fullyQualifiedHostNames() {
         var provider = new PortPerBrokerClusterNetworkAddressConfigProvider(
-                new PortPerBrokerClusterNetworkAddressConfigProviderConfig(parse("cluster.kafka.example.com:1235"), "broker.kafka.example.com:$(portNumber)", 1236,
+                new PortPerBrokerClusterNetworkAddressConfigProviderConfig(parse("cluster.kafka.example.com:1235"),
+                        "broker.kafka.example.com:$(portNumber)", 1236,
                         1238));
         assertThat(provider.getClusterBootstrapAddress()).isEqualTo(parse("cluster.kafka.example.com:1235"));
         assertThat(provider.getBrokerAddress(0)).isEqualTo(parse("broker.kafka.example.com:1236"));
@@ -114,7 +120,8 @@ class PortPerBrokerClusterNetworkAddressConfigProviderTest {
     @Test
     void fullyQualifiedHostNamesWithNodeInterpolation() {
         var provider = new PortPerBrokerClusterNetworkAddressConfigProvider(
-                new PortPerBrokerClusterNetworkAddressConfigProviderConfig(parse("cluster.kafka.example.com:1235"), "broker$(nodeId).kafka.example.com:$(portNumber)",
+                new PortPerBrokerClusterNetworkAddressConfigProviderConfig(parse("cluster.kafka.example.com:1235"),
+                        "broker$(nodeId).kafka.example.com:$(portNumber)",
                         1236, 1238));
         assertThat(provider.getClusterBootstrapAddress()).isEqualTo(parse("cluster.kafka.example.com:1235"));
         assertThat(provider.getBrokerAddress(0)).isEqualTo(parse("broker0.kafka.example.com:1236"));
diff --git a/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/SniRoutingClusterNetworkAddressConfigProviderTest.java b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/SniRoutingClusterNetworkAddressConfigProviderTest.java
index 92b30bc6..f0d503cb 100644
--- a/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/SniRoutingClusterNetworkAddressConfigProviderTest.java
+++ b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/clusternetworkaddressconfigprovider/SniRoutingClusterNetworkAddressConfigProviderTest.java
@@ -11,9 +11,9 @@ import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.NullAndEmptySource;
 import org.junit.jupiter.params.provider.ValueSource;
 
-import io.kroxylicious.proxy.internal.clusternetworkaddressconfigprovider.SniRoutingClusterNetworkAddressConfigProvider.SniRoutingClusterNetworkAddressConfigProviderConfig;
 import io.kroxylicious.proxy.service.HostPort;
 
+import static io.kroxylicious.proxy.internal.clusternetworkaddressconfigprovider.SniRoutingClusterNetworkAddressConfigProvider.SniRoutingClusterNetworkAddressConfigProviderConfig;
 import static io.kroxylicious.proxy.service.HostPort.parse;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -31,19 +31,22 @@ class SniRoutingClusterNetworkAddressConfigProviderTest {
     @ValueSource(strings = { "notgood", "toomany$(nodeId)$(nodeId)", "toomanyrecursive$(nodeId$(nodeId))", "shouty$(NODEID)badtoo" })
     @NullAndEmptySource
     void invalidBrokerPattern(String input) {
-        assertThrows(IllegalArgumentException.class, () -> new SniRoutingClusterNetworkAddressConfigProviderConfig(parse("good:1235"), input));
+        assertThrows(IllegalArgumentException.class,
+                () -> new SniRoutingClusterNetworkAddressConfigProviderConfig(parse("good:1235"), input));
     }
 
     @Test
     void getBrokerAddress() {
         var provider = new SniRoutingClusterNetworkAddressConfigProvider(
-                new SniRoutingClusterNetworkAddressConfigProviderConfig(parse("boot.kafka:1234"), "broker-$(nodeId).kafka"));
+                new SniRoutingClusterNetworkAddressConfigProviderConfig(parse("boot.kafka:1234"),
+                        "broker-$(nodeId).kafka"));
         assertThat(provider.getBrokerAddress(0)).isEqualTo(HostPort.parse("broker-0.kafka:1234"));
     }
 
     @Test
     void badNodeId() {
         assertThrows(IllegalArgumentException.class, () -> new SniRoutingClusterNetworkAddressConfigProvider(
-                new SniRoutingClusterNetworkAddressConfigProviderConfig(parse("boot.kafka:1234"), "broker-$(nodeId).kafka")).getBrokerAddress(-1));
+                new SniRoutingClusterNetworkAddressConfigProviderConfig(parse("boot.kafka:1234"), "broker-$(nodeId).kafka"))
+                .getBrokerAddress(-1));
     }
 }
\ No newline at end of file
diff --git a/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/filter/BrokerAddressFilterTest.java b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/filter/BrokerAddressFilterTest.java
index 8a32948f..15c62296 100644
--- a/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/filter/BrokerAddressFilterTest.java
+++ b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/filter/BrokerAddressFilterTest.java
@@ -34,11 +34,11 @@ import org.mockito.junit.jupiter.MockitoExtension;
 import com.flipkart.zjsonpatch.JsonDiff;
 import com.google.common.reflect.ClassPath;
 
-import io.kroxylicious.proxy.config.VirtualCluster;
 import io.kroxylicious.proxy.filter.FilterInvoker;
 import io.kroxylicious.proxy.filter.FilterInvokers;
 import io.kroxylicious.proxy.filter.KrpcFilterContext;
 import io.kroxylicious.proxy.internal.net.EndpointReconciler;
+import io.kroxylicious.proxy.model.VirtualCluster;
 import io.kroxylicious.proxy.service.HostPort;
 import io.kroxylicious.test.requestresponsetestdef.ApiMessageTestDef;
 import io.kroxylicious.test.requestresponsetestdef.RequestResponseTestDef;
diff --git a/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/net/EndpointRegistryTest.java b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/net/EndpointRegistryTest.java
index 83c24f99..39fcb953 100644
--- a/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/net/EndpointRegistryTest.java
+++ b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/net/EndpointRegistryTest.java
@@ -33,7 +33,7 @@ import io.netty.util.AttributeKey;
 
 import io.kroxylicious.proxy.HostPortConverter;
 import io.kroxylicious.proxy.config.TargetCluster;
-import io.kroxylicious.proxy.config.VirtualCluster;
+import io.kroxylicious.proxy.model.VirtualCluster;
 import io.kroxylicious.proxy.service.HostPort;
 
 import static org.assertj.core.api.Assertions.assertThat;