diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..76b8b44e
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,19 @@
+# EditorConfig helps developers define and maintain consistent
+# coding styles between different editors and IDEs
+# editorconfig.org
+
+root = true
+
+[*]
+# Change these settings to your own preference
+indent_style = space
+indent_size = 2
+# We recommend you to keep these unchanged
+end_of_line = lf
+insert_final_newline = true
+charset = utf-8
+trim_trailing_whitespace = true
+max_line_length = 140
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..a217b347
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,7 @@
+version: 2
+updates:
+- package-ecosystem: maven
+  directory: "/"
+  schedule:
+    interval: daily
+  open-pull-requests-limit: 10
diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml
new file mode 100644
index 00000000..e29f6f92
--- /dev/null
+++ b/.github/workflows/development.yml
@@ -0,0 +1,61 @@
+name: Development branches
+
+on:
+  push:
+  pull_request:
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    name: Build and run tests on JDK 11
+    steps:
+      # Checkout the code
+      - name: Checkout code
+        uses: actions/checkout@v2
+
+      - name: Expose branch name
+        run: echo ${{ github.ref }}
+
+      # Setup the cache
+      - name: Cache .m2
+        uses: actions/cache@v1
+        with:
+          path: ~/.m2/repository
+          key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+          restore-keys: |
+            ${{ runner.os }}-maven
+
+      # Setup JDK and Maven
+      - name: Set up JDK 11
+        uses: actions/setup-java@v1
+        with:
+          java-version: 11
+          server-id: ossrh
+          server-username: OSS_CENTRAL_USERNAME # env variable for Maven Central
+          server-password: OSS_CENTRAL_PASSWORD # env variable for Maven Central
+
+      # Prepare
+      - name: Prepare Maven Wrapper
+        run: chmod +x ./mvnw
+
+      # Build
+      - name: Build with Maven
+        run: ./mvnw clean verify -U -B -T4
+
+      # I-Test
+      - name: Run I-Test
+        run: ./mvnw verify -Pitest -U -B -T4
+
+      # Code analysis
+      - name: Upload coverage to Codecov
+        if: github.event_name == 'push' && github.actor != 'dependabot[bot]'
+        uses: codecov/codecov-action@v1.0.2
+        with:
+          token: ${{secrets.CODECOV_TOKEN}}
+
+      - name: Upload coverage to Codacy
+        if: github.event_name == 'push' && github.actor != 'dependabot[bot]'
+        uses: codacy/codacy-coverage-reporter-action@v1
+        with:
+          project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
+          coverage-reports: lib/coverage-aggregate/target/site/jacoco-aggregate/jacoco.xml
diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml
new file mode 100644
index 00000000..8d9e4017
--- /dev/null
+++ b/.github/workflows/master.yml
@@ -0,0 +1,70 @@
+name: Produces and releases artifacts
+
+on:
+  push:
+    branches:
+      - master
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    name: Build and run tests on JDK ${{ matrix.java }}
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v2
+
+      # Restore the cache first
+      - name: Cache .m2
+        uses: actions/cache@v1
+        with:
+          path: ~/.m2/repository
+          key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+          restore-keys: |
+            ${{ runner.os }}-maven
+
+      # Get GPG private key into GPG
+      - name: Import GPG Owner Trust
+        run: echo ${{ secrets.GPG_OWNERTRUST }} | base64 --decode | gpg --import-ownertrust
+
+      - name: Import GPG key
+        run: echo ${{ secrets.GPG_SECRET_KEYS }} | base64 --decode | gpg --import --no-tty --batch --yes
+
+      # Setup JDK and .m2/settings.xml
+      - name: Set up JDK 11
+        uses: actions/setup-java@v1
+        with:
+          java-version: 11
+          server-id: ossrh
+          server-username: OSS_CENTRAL_USERNAME # env variable for Maven Central
+          server-password: OSS_CENTRAL_PASSWORD # env variable for Maven Central
+
+      # Prepare
+      - name: Prepare Maven Wrapper
+        run: chmod +x ./mvnw
+
+      # Build
+      - name: Build with Maven
+        run: ./mvnw clean verify -U -B -T4
+
+      # I-Test
+      - name: Run I-Test
+        run: ./mvnw verify -Pitest -U -B -T4
+
+      # Publish release
+      - name: Deploy a new release version to Maven Central
+        run: ./mvnw clean deploy -B -DskipTests -DskipExamples -Prelease -Dgpg.keyname="${{ secrets.GPG_KEYNAME }}" -Dgpg.passphrase="${{ secrets.GPG_PASSPHRASE }}"
+        env:
+          OSS_CENTRAL_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
+          OSS_CENTRAL_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
+
+      # Code analysis
+      - name: Upolad coverage information
+        uses: codecov/codecov-action@v2
+        with:
+          token: ${{ secrets.CODECOV_TOKEN }}
+
+      - name: Upload coverage to Codacy
+        uses: codacy/codacy-coverage-reporter-action@v1
+        with:
+          project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
+          coverage-reports: lib/coverage-aggregate/target/site/jacoco-aggregate/jacoco.xml
diff --git a/.gitignore b/.gitignore
index 0e13eebb..d5f6528c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,26 @@ buildNumber.properties
 .mvn/timing.properties
 # https://github.com/takari/maven-wrapper#usage-without-binary-jar
 .mvn/wrapper/maven-wrapper.jar
+
+_tmp/
+_old/
+
+.attach_*
+*.iml
+.idea/
+.classpath
+.project
+.settings/
+.meta-data/
+.java-version
+.DS_Store
+.vscode/
+
+**/.tmp/
+.repository
+
+docs/
+
+javacore.*.txt
+Snap.*.trc
+
diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java
new file mode 100644
index 00000000..b901097f
--- /dev/null
+++ b/.mvn/wrapper/MavenWrapperDownloader.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2007-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import java.net.*;
+import java.io.*;
+import java.nio.channels.*;
+import java.util.Properties;
+
+public class MavenWrapperDownloader {
+
+    private static final String WRAPPER_VERSION = "0.5.6";
+    /**
+     * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
+     */
+    private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+        + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
+
+    /**
+     * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
+     * use instead of the default one.
+     */
+    private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
+            ".mvn/wrapper/maven-wrapper.properties";
+
+    /**
+     * Path where the maven-wrapper.jar will be saved to.
+     */
+    private static final String MAVEN_WRAPPER_JAR_PATH =
+            ".mvn/wrapper/maven-wrapper.jar";
+
+    /**
+     * Name of the property which should be used to override the default download url for the wrapper.
+     */
+    private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
+
+    public static void main(String args[]) {
+        System.out.println("- Downloader started");
+        File baseDirectory = new File(args[0]);
+        System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
+
+        // If the maven-wrapper.properties exists, read it and check if it contains a custom
+        // wrapperUrl parameter.
+        File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
+        String url = DEFAULT_DOWNLOAD_URL;
+        if(mavenWrapperPropertyFile.exists()) {
+            FileInputStream mavenWrapperPropertyFileInputStream = null;
+            try {
+                mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
+                Properties mavenWrapperProperties = new Properties();
+                mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
+                url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
+            } catch (IOException e) {
+                System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
+            } finally {
+                try {
+                    if(mavenWrapperPropertyFileInputStream != null) {
+                        mavenWrapperPropertyFileInputStream.close();
+                    }
+                } catch (IOException e) {
+                    // Ignore ...
+                }
+            }
+        }
+        System.out.println("- Downloading from: " + url);
+
+        File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
+        if(!outputFile.getParentFile().exists()) {
+            if(!outputFile.getParentFile().mkdirs()) {
+                System.out.println(
+                        "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
+            }
+        }
+        System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
+        try {
+            downloadFileFromURL(url, outputFile);
+            System.out.println("Done");
+            System.exit(0);
+        } catch (Throwable e) {
+            System.out.println("- Error downloading");
+            e.printStackTrace();
+            System.exit(1);
+        }
+    }
+
+    private static void downloadFileFromURL(String urlString, File destination) throws Exception {
+        if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
+            String username = System.getenv("MVNW_USERNAME");
+            char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
+            Authenticator.setDefault(new Authenticator() {
+                @Override
+                protected PasswordAuthentication getPasswordAuthentication() {
+                    return new PasswordAuthentication(username, password);
+                }
+            });
+        }
+        URL website = new URL(urlString);
+        ReadableByteChannel rbc;
+        rbc = Channels.newChannel(website.openStream());
+        FileOutputStream fos = new FileOutputStream(destination);
+        fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+        fos.close();
+        rbc.close();
+    }
+
+}
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 00000000..ffdc10e5
--- /dev/null
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,2 @@
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.1/apache-maven-3.8.1-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
diff --git a/README.md b/README.md
index 3a40e471..152bd3ad 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,29 @@
-# avro-adapter
+# avro-registry-adapter
+
+[![Build Status](https://github.com/holixon/avro-registry-adapter/workflows/Development%20branches/badge.svg)](https://github.com/holixon/avro-registry-adapter/actions)
+[![Codacy Badge](https://app.codacy.com/project/badge/Grade/1f099f2971ed401ea1d8d55a7183a00c)](https://www.codacy.com/gh/holixon/avro-registry-adapter/dashboard?utm_source=github.com&utm_medium=referral&utm_content=holixon/avro-registry-adapter&utm_campaign=Badge_Grade)
+[![codecov](https://codecov.io/gh/holixon/avro-registry-adapter/branch/develop/graph/badge.svg?token=bjT1hlfnH4)](https://codecov.io/gh/holixon/avro-registry-adapter)
+[![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.holixon.avro/avro-registry-adapter-bom/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.holixon.avro/avro-registry-adapter-bom)
+[![sponsored](https://img.shields.io/badge/sponsoredBy-Holisticon-RED.svg)](https://holisticon.de/)
+
 Convenient support for working with avro serialization on the JVM
+
+## Modules
+
+### `avro-registry-adapter-bom`
+
+Bill of material that lists all modules for convenient dependency definition.
+
+### `avro-registry-adapter-api`
+
+Defines core interfaces and helpers to simplify and generalize working with apache avro.
+These interfaces do not rely on the default serialization of the avro lib, so they can be implemented against various
+registries and serialization strategies.
+
+### `avro-registry-adapter-default`
+
+Implements `avro-registry-adapter-api` following the default avro lib serialization and deserialization specifications.
+
+### `avro-registry-adapter-apicurio-rest`
+
+Implements a [AvroSchemaRegistry](./extension/api/src/main/kotlin/AvroSchemaRegistry.kt) that uses the [apicurio-registry-client](https://github.com/Apicurio/apicurio-registry/tree/master/client) to connect to the apicurio registry via REST.
diff --git a/bom/pom.xml b/bom/pom.xml
new file mode 100644
index 00000000..58934b4f
--- /dev/null
+++ b/bom/pom.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>io.holixon.avro._</groupId>
+    <artifactId>avro-registry-adapter</artifactId>
+    <version>0.0.1</version>
+  </parent>
+
+  <groupId>io.holixon.avro</groupId>
+  <artifactId>avro-registry-adapter-bom</artifactId>
+  <packaging>pom</packaging>
+
+  <properties>
+    <avro-registry-adapter.version>${project.version}</avro-registry-adapter.version>
+    <avro.version>1.10.2</avro.version>
+  </properties>
+
+  <dependencyManagement>
+    <dependencies>
+      <dependency>
+        <artifactId>avro-registry-adapter-api</artifactId>
+        <groupId>${project.groupId}</groupId>
+        <version>${avro-registry-adapter.version}</version>
+      </dependency>
+
+      <dependency>
+        <artifactId>avro-registry-adapter-default</artifactId>
+        <groupId>${project.groupId}</groupId>
+        <version>${avro-registry-adapter.version}</version>
+      </dependency>
+
+      <dependency>
+        <artifactId>avro-registry-adapter-apicurio</artifactId>
+        <groupId>${project.groupId}</groupId>
+        <version>${avro-registry-adapter.version}</version>
+      </dependency>
+
+      <!-- AVRO -->
+      <dependency>
+        <groupId>org.apache.avro</groupId>
+        <artifactId>avro</artifactId>
+        <version>${avro.version}</version>
+        <scope>provided</scope>
+      </dependency>
+
+    </dependencies>
+  </dependencyManagement>
+
+  <build>
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <groupId>org.apache.avro</groupId>
+          <artifactId>avro-maven-plugin</artifactId>
+          <version>${avro.version}</version>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+  </build>
+</project>
diff --git a/detekt.yml b/detekt.yml
new file mode 100644
index 00000000..bb023ed2
--- /dev/null
+++ b/detekt.yml
@@ -0,0 +1,26 @@
+#
+# see https://github.com/arturbosch/detekt/blob/master/detekt-cli/src/main/resources/default-detekt-config.yml
+#
+comments:
+  active: true
+  excludes: "examples/**,**/test/**,**/examples/**,**/*Test.kt,*/src/test/kotlin/**/*.kt,**/*Stages.kt,**/*ITest.kt"
+  CommentOverPrivateFunction:
+    active: false
+  CommentOverPrivateProperty:
+    active: false
+  EndOfSentenceFormat:
+    active: false
+    endOfSentenceFormat: ([.?!][ \t\n\r\f<])|([.?!:]$)
+  UndocumentedPublicClass:
+    active: true
+    searchInNestedClass: true
+    searchInInnerClass: true
+    searchInInnerObject: true
+    searchInInnerInterface: true
+  UndocumentedPublicFunction:
+    active: true
+
+performance:
+  active: true
+  SpreadOperator:
+    active: false
diff --git a/examples/docker-compose.yml b/examples/docker-compose.yml
new file mode 100644
index 00000000..dc12fa93
--- /dev/null
+++ b/examples/docker-compose.yml
@@ -0,0 +1,6 @@
+version: '3.1'
+services:
+  registry:
+    image: apicurio/apicurio-registry-mem:2.0.0.Final
+    ports:
+      - "7777:8080"
diff --git a/examples/pom.xml b/examples/pom.xml
new file mode 100644
index 00000000..26051a10
--- /dev/null
+++ b/examples/pom.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>io.holixon.avro._</groupId>
+    <artifactId>avro-registry-adapter-parent</artifactId>
+    <version>0.0.1</version>
+    <relativePath>../parent/pom.xml</relativePath>
+  </parent>
+
+  <artifactId>examples</artifactId>
+  <packaging>pom</packaging>
+
+  <properties>
+    <jacoco.skip>true</jacoco.skip>
+    <maven.javadoc.skip>true</maven.javadoc.skip>
+    <maven.sources.skip>true</maven.sources.skip>
+    <maven.install.skip>true</maven.install.skip>
+    <gpg.skip>true</gpg.skip>
+
+    <springdoc.version>1.5.6</springdoc.version>
+  </properties>
+
+  <dependencies>
+    <!-- No dependencies here, define explicitly in example -->
+  </dependencies>
+  
+</project>
diff --git a/extension/api/pom.xml b/extension/api/pom.xml
new file mode 100644
index 00000000..00cf08ee
--- /dev/null
+++ b/extension/api/pom.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>io.holixon.avro._</groupId>
+    <artifactId>extension</artifactId>
+    <version>0.0.1</version>
+  </parent>
+
+  <groupId>io.holixon.avro</groupId>
+  <artifactId>avro-registry-adapter-api</artifactId>
+  <description>API of the registry and Avro helpers.</description>
+
+  <properties>
+    <jacoco.skip>false</jacoco.skip>
+  </properties>
+</project>
diff --git a/extension/api/src/main/kotlin/AvroAdapterApi.kt b/extension/api/src/main/kotlin/AvroAdapterApi.kt
new file mode 100644
index 00000000..b82a06fc
--- /dev/null
+++ b/extension/api/src/main/kotlin/AvroAdapterApi.kt
@@ -0,0 +1,78 @@
+package io.holixon.avro.adapter.api
+
+import io.holixon.avro.adapter.api.type.AvroSchemaInfoData
+import org.apache.avro.Schema
+import org.apache.avro.specific.SpecificData
+import java.nio.ByteBuffer
+import java.nio.charset.StandardCharsets
+import java.util.*
+import java.util.function.BiFunction
+import java.util.function.Function
+import java.util.function.Predicate
+import kotlin.reflect.KClass
+
+/**
+ * Returns a unique id for the given schema that is used to load the schema from a repository.
+ */
+fun interface SchemaIdSupplier : Function<Schema, AvroSchemaId>
+
+/**
+ * Search schema based on schemaId.
+ */
+fun interface SchemaResolver : Function<AvroSchemaId, Optional<AvroSchemaWithId>>
+
+/**
+ * Takes encoded avro bytes (containing schema reference) and return decoded payload and the resolved schema.
+ */
+fun interface SingleObjectDecoder : Function<AvroSingleObjectEncoded, AvroPayloadAndSchema>
+
+/**
+ * Use given Schema information and bytecode payload to return decoded bytes.
+ */
+fun interface SingleObjectEncoder : BiFunction<AvroSchemaWithId, ByteArray, AvroSingleObjectEncoded>
+
+/**
+ * Returns the revision for a given schema.
+ */
+fun interface SchemaRevisionResolver : Function<Schema, Optional<AvroSchemaRevision>>
+
+/**
+ * Returns `true` if the [ByteBuffer] conforms to the singleObject encoding specification.
+ */
+fun interface IsAvroSingleObjectEncodedPredicate : Predicate<ByteBuffer>
+
+/**
+ * Global utility methods.
+ */
+object AvroAdapterApi {
+
+  fun Schema.byteContent() = this.toString().byteInputStream(StandardCharsets.UTF_8)
+
+  /**
+   * Determines the revision of a given schema by reading the String value of the given object-property.
+   */
+  @JvmStatic
+  fun propertyBasedSchemaRevisionResolver(propertyKey: String): SchemaRevisionResolver =
+    SchemaRevisionResolver { schema -> Optional.ofNullable(schema.getObjectProp(propertyKey) as String?) }
+
+  @JvmStatic
+  fun schemaForClass(recordClass: Class<*>): Schema = SpecificData(recordClass.classLoader).getSchema(recordClass)
+
+  @JvmStatic
+  fun schemaForClass(recordClass: KClass<*>) = schemaForClass(recordClass.java)
+
+  @JvmStatic
+  fun Schema.extractSchemaInfo(schemaRevisionResolver: SchemaRevisionResolver) = AvroSchemaInfoData(
+    namespace = namespace,
+    name = name,
+    revision = schemaRevisionResolver.apply(this).orElse(null)
+  )
+
+  /**
+   * Creates a schema resolver out of a read-only registry.
+   * @return [SchemaResolver] derived from registry.
+   */
+  fun AvroSchemaReadOnlyRegistry.schemaResolver() = SchemaResolver { schemaId -> this@schemaResolver.findById(schemaId) }
+
+}
+
diff --git a/extension/api/src/main/kotlin/AvroAdapterTypes.kt b/extension/api/src/main/kotlin/AvroAdapterTypes.kt
new file mode 100644
index 00000000..1f1b3ab2
--- /dev/null
+++ b/extension/api/src/main/kotlin/AvroAdapterTypes.kt
@@ -0,0 +1,149 @@
+package io.holixon.avro.adapter.api
+
+import org.apache.avro.Schema
+
+/**
+ * The unique id of a schema artifact, published to a repo.
+ */
+typealias AvroSchemaId = String
+
+/**
+ * The version of a schema.
+ */
+typealias AvroSchemaRevision = String
+
+/**
+ * Message encoded as [Single Object](https://avro.apache.org/docs/current/spec.html#single_object_encoding) ByteArray.
+ */
+typealias AvroSingleObjectEncoded = ByteArray
+
+/**
+ * The encoded message. This is only the payload data,
+ * so no marker header and encoded schemaId are present.
+ */
+typealias AvroSingleObjectPayload = ByteArray
+
+/**
+ * Wrapper type for [AvroSchemaId] and the encoded message [AvroSingleObjectPayload].
+ */
+interface AvroPayloadAndSchemaId {
+  val schemaId: AvroSchemaId
+  val payload: AvroSingleObjectPayload
+
+  operator fun component1() = schemaId
+  operator fun component2() = payload
+}
+
+/**
+ * Wrapper type containing the [AvroSingleObjectPayload], the [Schema] and the artifacts [AvroSchemaId].
+ */
+interface AvroPayloadAndSchema {
+  val schema: AvroSchemaWithId
+  val payload: AvroSingleObjectPayload
+
+  operator fun component1() = schema
+  operator fun component2() = payload
+}
+
+/**
+ * The schema info provides the relevant identifiers for a schema:
+ *
+ * * context (aka namespace)
+ * * name
+ * * revision
+ *
+ */
+interface AvroSchemaInfo {
+
+  companion object {
+    /**
+     * Default separator used in canonical name.
+     */
+    const val NAME_SEPARATOR = "."
+  }
+
+  /**
+   * Schema namespace.
+   */
+  val namespace: String
+
+  /**
+   * Schema name.
+   */
+  val name: String
+
+  /**
+   * Optional revision.
+   */
+  val revision: AvroSchemaRevision?
+
+  /**
+   * Canonical schema revision.
+   */
+  val canonicalName: String
+    get() = "$namespace$NAME_SEPARATOR$name"
+}
+
+/**
+ * Tuple wrapping the schema and its id.
+ */
+interface AvroSchemaWithId : AvroSchemaInfo {
+  /**
+   * Id of the schema.
+   */
+  val schemaId: AvroSchemaId
+
+  /**
+   * Avro schema.
+   */
+  val schema: Schema
+
+  operator fun component1() = schemaId
+  operator fun component2() = schema
+  operator fun component3() = revision
+}
+
+// FIXME: implement meta
+// interface AvroSchemaMeta {
+//  val name: String?
+//
+//  val description: String?
+//
+//  val labels: List<String>?
+//
+//  @JsonProperty("createdBy")
+//  private val createdBy: String? = null
+//
+//  @JsonProperty("createdOn")
+//  private val createdOn: Long = 0
+//
+//  @JsonProperty("modifiedBy")
+//  private val modifiedBy: String? = null
+//
+//  @JsonProperty("modifiedOn")
+//  private val modifiedOn: Long = 0
+//
+//  @JsonProperty("id")
+//  @JsonPropertyDescription("")
+//  private val id: String? = null
+//
+//  @JsonProperty("version")
+//  @JsonPropertyDescription("")
+//  private val version: Int? = null
+//
+//  @JsonProperty("type")
+//  @JsonPropertyDescription("")
+//  private val type: io.apicurio.registry.types.ArtifactType? = null
+//
+//  @JsonProperty("globalId")
+//  @JsonPropertyDescription("")
+//  private val globalId: Long? = null
+//
+//  @JsonProperty("state")
+//  @JsonPropertyDescription("Describes the state of an artifact or artifact version.  The following states\nare possible:\n\n* ENABLED\n* DISABLED\n* DEPRECATED\n")
+//  private val state: ArtifactState? = null
+//
+//  @JsonProperty("properties")
+//  @JsonPropertyDescription("A set of name-value properties for an artifact or artifact version.")
+//  private val properties: Map<String, String>? = null
+// }
diff --git a/extension/api/src/main/kotlin/AvroSchemaIncompatibilityResolver.kt b/extension/api/src/main/kotlin/AvroSchemaIncompatibilityResolver.kt
new file mode 100644
index 00000000..e731a0e7
--- /dev/null
+++ b/extension/api/src/main/kotlin/AvroSchemaIncompatibilityResolver.kt
@@ -0,0 +1,19 @@
+package io.holixon.avro.adapter.api
+
+import org.apache.avro.Schema
+import org.apache.avro.SchemaCompatibility
+import org.apache.avro.message.SchemaStore
+
+/**
+ * Resolves incompatibility check result and delivers the reader schema used by decoding.
+ */
+fun interface AvroSchemaIncompatibilityResolver {
+  /**
+   * Resolves possible schema incompatibility.
+   * @return reader schema to use.
+   */
+  fun resolve(
+      readerSchema: Schema,
+      writerSchema: Schema
+  ): Schema
+}
diff --git a/extension/api/src/main/kotlin/AvroSchemaRegistry.kt b/extension/api/src/main/kotlin/AvroSchemaRegistry.kt
new file mode 100644
index 00000000..f3d2b8af
--- /dev/null
+++ b/extension/api/src/main/kotlin/AvroSchemaRegistry.kt
@@ -0,0 +1,42 @@
+package io.holixon.avro.adapter.api
+
+import org.apache.avro.Schema
+import java.util.*
+
+/**
+ * The Schema Registry is responsible for storing and retrieving arvo schema files.
+ */
+interface AvroSchemaRegistry : AvroSchemaReadOnlyRegistry {
+
+  /**
+   * Stores a new [Schema] (version) in the repository.
+   */
+  fun register(schema: Schema): AvroSchemaWithId
+
+}
+
+/**
+ * The Schema Registry is responsible for retrieving arvo schema files.
+ */
+interface AvroSchemaReadOnlyRegistry {
+  /**
+   * Finds a stored [Schema] based on its unique [AvroSchemaId] (e.g. its fingerprint).
+   */
+  fun findById(schemaId: AvroSchemaId): Optional<AvroSchemaWithId>
+
+  /**
+   * Finds a stored [Schema] based on its derived info.
+   */
+  fun findByInfo(info: AvroSchemaInfo): Optional<AvroSchemaWithId>
+
+  /**
+   * Finds all stored [Schema]s based on their namespaces and names (e.g. FQN).
+   */
+  fun findAllByCanonicalName(namespace: String, name: String): List<AvroSchemaWithId>
+
+  /**
+   * Simply lists all stored [Schema]s.
+   */
+  fun findAll(): List<AvroSchemaWithId>
+
+}
diff --git a/extension/api/src/main/kotlin/converter/SpecificRecordToSingleObjectConverter.kt b/extension/api/src/main/kotlin/converter/SpecificRecordToSingleObjectConverter.kt
new file mode 100644
index 00000000..eda7e953
--- /dev/null
+++ b/extension/api/src/main/kotlin/converter/SpecificRecordToSingleObjectConverter.kt
@@ -0,0 +1,22 @@
+package io.holixon.avro.adapter.api.converter
+
+import io.holixon.avro.adapter.api.AvroSingleObjectEncoded
+import org.apache.avro.specific.SpecificRecordBase
+
+/**
+ * Converts a typed SpecificRecord to single object bytes.
+ */
+interface SpecificRecordToSingleObjectConverter {
+
+  /**
+   * Converts instance of SpecificRecord to bytes containing the SchemaId.
+   */
+  fun <T: SpecificRecordBase> encode(data: T) : AvroSingleObjectEncoded
+
+  /**
+   * Extracts SchemaId from given bytes amd converts the contents of the bytes payload
+   * to a SpecificRecord instance of that writer schema.
+   */
+  fun <T: SpecificRecordBase> decode(bytes: AvroSingleObjectEncoded) : T
+
+}
diff --git a/extension/api/src/main/kotlin/ext/ByteArrayExt.kt b/extension/api/src/main/kotlin/ext/ByteArrayExt.kt
new file mode 100644
index 00000000..37eb7f44
--- /dev/null
+++ b/extension/api/src/main/kotlin/ext/ByteArrayExt.kt
@@ -0,0 +1,77 @@
+package io.holixon.avro.adapter.api.ext
+
+import java.nio.ByteBuffer
+
+/**
+ * Extension functions for [ByteArray] and [ByteBuffer] that simplify
+ * with avro encoded data.
+ */
+object ByteArrayExt {
+
+  /**
+   * Converts a byte array into its hexadecimal string representation
+   * e.g. for the V1_HEADER => [C3 01]
+   *
+   * @param separator - what to print between the bytes, defaults to " "
+   * @param prefix - start of string, defaults to "["
+   * @param suffix - end of string, defaults to "]"
+   * @return the hexadecimal string representation of the input byte array
+   */
+  @JvmStatic
+  fun ByteArray.toHexString(): String = this.joinToString(
+    separator = " ",
+    prefix = "[",
+    postfix = "]"
+  ) { "%02X".format(it) }
+
+  /**
+   * @return [ByteBuffer.wrap] for given array
+   */
+  fun ByteArray.buffer(): ByteBuffer = ByteBuffer.wrap(this)
+
+  /**
+   * Splits an array in `n+1` parts, where `n` is the number of given indexes.
+   * The first slice reaches from 0 to index_0, the last slice from index_n to end.
+   *
+   * @param indexes - positive ints, must be sorted
+   * @return list of `n+1` byte arrays, each having a size of the diff between the indexes.
+   */
+  fun ByteArray.split(vararg indexes: Int): List<ByteArray> {
+    require(indexes.none { it < 0 || it > size - 1 }) { "all indexes have to match '0 < index < size-1', was: indexes=${indexes.toList()}, size=$size" }
+    require(indexes.toList() == indexes.sorted()) { "indexes must be ordered, was: ${indexes.toList()}" }
+    require(indexes.size == indexes.distinct().size) { "indexes must be unique, was: ${indexes.toList()}" }
+
+    val allButLast =
+      indexes.fold(0 to emptyList<ByteArray>()) { pair, nextIndex -> nextIndex to pair.second + this.copyOfRange(pair.first, nextIndex) }
+    return allButLast.second + this.copyOfRange(allButLast.first, size)
+  }
+
+  /**
+   * @see ByteArray.split(indexes)
+   */
+  fun ByteBuffer.split(vararg indexes: Int): List<ByteArray> = this.array().split(*indexes)
+
+  /**
+   * Reads [size] bytes from given buffer, starting at given position.
+   * Ensures that the original buffer position is kept.
+   *
+   * @param position - the position to start reading
+   * @param size - how many bytes should be read. if not given, all remaining bytes are read
+   * @return bytesArray containg [size] bytes starting at [position]
+   */
+  fun ByteBuffer.extract(position: Int, size: Int? = null): ByteArray {
+    val originalPosition = this.position()
+    try {
+      this.position(position)
+      require(size == null || size > 0) { "size < 1: ($size < 1)" }
+      val maxSize = remaining()
+      require(size == null || size <= maxSize) { "Cannot extract from position=$position, size=$size, remaining=${this.remaining()}" }
+
+      val bytes = ByteArray(size ?: this.remaining())
+      this.get(bytes)
+      return bytes
+    } finally {
+      this.position(originalPosition)
+    }
+  }
+}
diff --git a/extension/api/src/main/kotlin/ext/FunctionalExt.kt b/extension/api/src/main/kotlin/ext/FunctionalExt.kt
new file mode 100644
index 00000000..3528d8bd
--- /dev/null
+++ b/extension/api/src/main/kotlin/ext/FunctionalExt.kt
@@ -0,0 +1,12 @@
+package io.holixon.avro.adapter.api.ext
+
+import java.util.function.Function
+
+/**
+ * Collection of function extensions.
+ */
+object FunctionalExt {
+
+  operator fun <T:Any, R:Any> Function<T,R>.invoke(t:T) :R = this.apply(t)
+
+}
diff --git a/extension/api/src/main/kotlin/type/AvroPayloadAndSchemaData.kt b/extension/api/src/main/kotlin/type/AvroPayloadAndSchemaData.kt
new file mode 100644
index 00000000..77e828f2
--- /dev/null
+++ b/extension/api/src/main/kotlin/type/AvroPayloadAndSchemaData.kt
@@ -0,0 +1,32 @@
+package io.holixon.avro.adapter.api.type
+
+import io.holixon.avro.adapter.api.AvroPayloadAndSchema
+import io.holixon.avro.adapter.api.AvroSchemaWithId
+import io.holixon.avro.adapter.api.AvroSingleObjectEncoded
+
+/**
+ * Data class implementing [AvroPayloadAndSchema].
+ */
+data class AvroPayloadAndSchemaData(
+  override val schema: AvroSchemaWithId,
+  override val payload: AvroSingleObjectEncoded
+) : AvroPayloadAndSchema {
+
+  override fun equals(other: Any?): Boolean {
+    if (this === other) return true
+    if (javaClass != other?.javaClass) return false
+
+    other as AvroPayloadAndSchemaData
+
+    if (schema != other.schema) return false
+    if (!payload.contentEquals(other.payload)) return false
+
+    return true
+  }
+
+  override fun hashCode(): Int {
+    var result = schema.hashCode()
+    result = 31 * result + payload.contentHashCode()
+    return result
+  }
+}
diff --git a/extension/api/src/main/kotlin/type/AvroPayloadAndSchemaIdData.kt b/extension/api/src/main/kotlin/type/AvroPayloadAndSchemaIdData.kt
new file mode 100644
index 00000000..6174297c
--- /dev/null
+++ b/extension/api/src/main/kotlin/type/AvroPayloadAndSchemaIdData.kt
@@ -0,0 +1,32 @@
+package io.holixon.avro.adapter.api.type
+
+import io.holixon.avro.adapter.api.AvroPayloadAndSchemaId
+import io.holixon.avro.adapter.api.AvroSingleObjectPayload
+import io.holixon.avro.adapter.api.AvroSchemaId
+
+/**
+ * Data class implementation of the [AvroPayloadAndSchemaId].
+ */
+data class AvroPayloadAndSchemaIdData(
+  override val schemaId : AvroSchemaId,
+  override val payload : AvroSingleObjectPayload
+) : AvroPayloadAndSchemaId {
+  override fun equals(other: Any?): Boolean {
+    if (this === other) return true
+    if (javaClass != other?.javaClass) return false
+
+    other as AvroPayloadAndSchemaIdData
+
+    if (schemaId != other.schemaId) return false
+    if (!payload.contentEquals(other.payload)) return false
+
+    return true
+  }
+
+  override fun hashCode(): Int {
+    var result = schemaId.hashCode()
+    result = 31 * result + payload.contentHashCode()
+    return result
+  }
+
+}
diff --git a/extension/api/src/main/kotlin/type/AvroSchemaInfoData.kt b/extension/api/src/main/kotlin/type/AvroSchemaInfoData.kt
new file mode 100644
index 00000000..16a14e18
--- /dev/null
+++ b/extension/api/src/main/kotlin/type/AvroSchemaInfoData.kt
@@ -0,0 +1,13 @@
+package io.holixon.avro.adapter.api.type
+
+import io.holixon.avro.adapter.api.AvroSchemaInfo
+import io.holixon.avro.adapter.api.AvroSchemaRevision
+
+/**
+ * Data class implementing [AvroSchemaInfo].
+ */
+data class AvroSchemaInfoData(
+  override val namespace: String,
+  override val name: String,
+  override val revision: AvroSchemaRevision?
+) : AvroSchemaInfo
diff --git a/extension/api/src/main/kotlin/type/AvroSchemaWithIdData.kt b/extension/api/src/main/kotlin/type/AvroSchemaWithIdData.kt
new file mode 100644
index 00000000..2249daac
--- /dev/null
+++ b/extension/api/src/main/kotlin/type/AvroSchemaWithIdData.kt
@@ -0,0 +1,29 @@
+package io.holixon.avro.adapter.api.type
+
+import io.holixon.avro.adapter.api.AvroSchemaWithId
+import io.holixon.avro.adapter.api.AvroSchemaId
+import io.holixon.avro.adapter.api.AvroSchemaRevision
+import org.apache.avro.Schema
+
+/**
+ * Data class implementing [AvroSchemaWithId].
+ */
+data class AvroSchemaWithIdData(
+  override val schemaId: AvroSchemaId,
+  override val schema: Schema,
+  override val revision: AvroSchemaRevision?,
+  override val namespace: String,
+  override val name: String
+) : AvroSchemaWithId {
+  constructor(
+    schemaId: AvroSchemaId,
+    schema: Schema,
+    revision: AvroSchemaRevision? = null
+  ) : this(
+    schemaId = schemaId,
+    schema = schema,
+    revision = revision,
+    namespace = schema.namespace,
+    name = schema.name
+  )
+}
diff --git a/extension/api/src/main/resources/.gitkeep b/extension/api/src/main/resources/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/extension/api/src/test/kotlin/AvroAdapterApiTestHelper.kt b/extension/api/src/test/kotlin/AvroAdapterApiTestHelper.kt
new file mode 100644
index 00000000..e9326f97
--- /dev/null
+++ b/extension/api/src/test/kotlin/AvroAdapterApiTestHelper.kt
@@ -0,0 +1,2 @@
+package io.holixon.avro.adapter.api
+
diff --git a/extension/api/src/test/kotlin/ext/ByteArrayExtTest.kt b/extension/api/src/test/kotlin/ext/ByteArrayExtTest.kt
new file mode 100644
index 00000000..e4e5c697
--- /dev/null
+++ b/extension/api/src/test/kotlin/ext/ByteArrayExtTest.kt
@@ -0,0 +1,80 @@
+package io.holixon.avro.adapter.api.ext
+
+import io.holixon.avro.adapter.api.ext.ByteArrayExt.extract
+import io.holixon.avro.adapter.api.ext.ByteArrayExt.split
+import io.holixon.avro.adapter.api.ext.ByteArrayExt.buffer
+import io.holixon.avro.adapter.api.ext.ByteArrayExt.toHexString
+import mu.KLogging
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.assertThatThrownBy
+import org.junit.jupiter.api.Test
+
+internal class ByteArrayExtTest {
+  companion object : KLogging()
+
+  private val helloBytes: ByteArray = "Hello World!".encodeToByteArray()
+
+  @Test
+  internal fun `to hex String`() {
+    assertThat(helloBytes.toHexString()).isEqualTo("[48 65 6C 6C 6F 20 57 6F 72 6C 64 21]")
+    assertThat(byteArrayOf(-61, 1).toHexString()).isEqualTo("[C3 01]")
+  }
+
+  @Test
+  internal fun `extract from byte buffer`() {
+    val buffer = helloBytes.buffer()
+    buffer.position(7)
+
+    // extract 3,4,5
+    assertThat(buffer.extract(2, 3).toHexString()).isEqualTo("[6C 6C 6F]")
+    assertThat(buffer.position()).isEqualTo(7)
+
+    // fails when extracting too much
+    assertThatThrownBy { buffer.extract(0, 100) }
+      .isInstanceOf(IllegalArgumentException::class.javaObjectType)
+      .hasMessage("Cannot extract from position=0, size=100, remaining=${helloBytes.size}")
+    assertThat(buffer.position()).isEqualTo(7)
+
+    // fails when extracting too much
+    assertThatThrownBy { buffer.extract(0, 100) }
+      .isInstanceOf(IllegalArgumentException::class.javaObjectType)
+      .hasMessage("Cannot extract from position=0, size=100, remaining=${helloBytes.size}")
+    assertThat(buffer.position()).isEqualTo(7)
+
+    assertThatThrownBy { byteArrayOf(-61).buffer().extract(1, 1) }
+      .isInstanceOf(IllegalArgumentException::class.javaObjectType)
+      .hasMessage("Cannot extract from position=1, size=1, remaining=0")
+
+    // fails when pos < 0
+    assertThatThrownBy { buffer.extract(-1, 5) }
+      .isInstanceOf(IllegalArgumentException::class.java)
+      .hasMessage("newPosition < 0: (-1 < 0)")
+
+    // fails when size < 1
+    assertThatThrownBy { buffer.extract(0, 0) }
+      .isInstanceOf(IllegalArgumentException::class.java)
+      .hasMessage("size < 1: (0 < 1)")
+
+    // extract after
+    assertThat(buffer.extract(9).toHexString()).isEqualTo("[6C 64 21]")
+  }
+
+  @Test
+  internal fun `split byteArray`() {
+    assertThatThrownBy { helloBytes.split(-1) }.hasMessage("all indexes have to match '0 < index < size-1', was: indexes=[-1], size=12")
+      .isInstanceOf(IllegalArgumentException::class.java)
+    assertThatThrownBy { helloBytes.split(100) }.hasMessage("all indexes have to match '0 < index < size-1', was: indexes=[100], size=12")
+      .isInstanceOf(IllegalArgumentException::class.java)
+    assertThatThrownBy { helloBytes.split(10, 5) }.hasMessage("indexes must be ordered, was: [10, 5]")
+      .isInstanceOf(IllegalArgumentException::class.java)
+    assertThatThrownBy { helloBytes.split(10, 10) }.hasMessage("indexes must be unique, was: [10, 10]")
+      .isInstanceOf(IllegalArgumentException::class.java)
+
+    val parts = helloBytes.split(5, 10)
+
+    assertThat(parts).hasSize(3)
+    assertThat(parts[0].toHexString()).isEqualTo("[48 65 6C 6C 6F]")
+    assertThat(parts[1].toHexString()).isEqualTo("[20 57 6F 72 6C]")
+    assertThat(parts[2].toHexString()).isEqualTo("[64 21]")
+  }
+}
diff --git a/extension/api/src/test/kotlin/type/AvroSchemaInfoTest.kt b/extension/api/src/test/kotlin/type/AvroSchemaInfoTest.kt
new file mode 100644
index 00000000..3fb4255d
--- /dev/null
+++ b/extension/api/src/test/kotlin/type/AvroSchemaInfoTest.kt
@@ -0,0 +1,21 @@
+package io.holixon.avro.adapter.api.type
+
+import io.holixon.avro.adapter.api.AvroSchemaInfo
+import io.holixon.avro.lib.test.AvroAdapterTestLib
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+
+internal class AvroSchemaInfoTest {
+
+  private val sampleEventSchema = AvroAdapterTestLib.loadArvoResource("test.fixture.SampleEvent-v4711")
+
+  @Test
+  internal fun `avro schema info`() {
+    val data: AvroSchemaInfo = AvroSchemaInfoData(namespace = "foo", name = "bar", revision = "1")
+
+    assertThat(data.namespace).isEqualTo("foo")
+    assertThat(data.name).isEqualTo("bar")
+    assertThat(data.revision).isEqualTo("1")
+    assertThat(data.canonicalName).isEqualTo("foo.bar")
+  }
+}
diff --git a/extension/api/src/test/kotlin/type/AvroSchemaWithIdTest.kt b/extension/api/src/test/kotlin/type/AvroSchemaWithIdTest.kt
new file mode 100644
index 00000000..f50f65e8
--- /dev/null
+++ b/extension/api/src/test/kotlin/type/AvroSchemaWithIdTest.kt
@@ -0,0 +1,30 @@
+package io.holixon.avro.adapter.api.type
+
+import io.holixon.avro.adapter.api.AvroAdapterApi
+import io.holixon.avro.lib.test.AvroAdapterTestLib
+import org.apache.avro.Schema
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+
+internal class AvroSchemaWithIdTest {
+
+  private val sampleEventSchema = AvroAdapterTestLib.loadArvoResource("test.fixture.SampleEvent-v4711")
+  private val schemaRevisionResolver = AvroAdapterApi.propertyBasedSchemaRevisionResolver("revision")
+
+  @Test
+  internal fun `read schema and derive values`() {
+    val schema = Schema.Parser().parse(sampleEventSchema)
+
+    val avroSchema = AvroSchemaWithIdData(
+      schemaId = "1",
+      schema = schema,
+      revision = schemaRevisionResolver.apply(schema).orElse(null)
+    )
+
+    assertThat(avroSchema.schemaId).isEqualTo("1")
+    assertThat(avroSchema.revision).isEqualTo("4711")
+    assertThat(avroSchema.name).isEqualTo("SampleEvent")
+    assertThat(avroSchema.namespace).isEqualTo("test.fixture")
+    assertThat(avroSchema.canonicalName).isEqualTo("test.fixture.SampleEvent")
+  }
+}
diff --git a/extension/api/src/test/resources/.gitkeep b/extension/api/src/test/resources/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/extension/default/pom.xml b/extension/default/pom.xml
new file mode 100644
index 00000000..9d7295a3
--- /dev/null
+++ b/extension/default/pom.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>io.holixon.avro._</groupId>
+    <artifactId>extension</artifactId>
+    <version>0.0.1</version>
+  </parent>
+
+  <groupId>io.holixon.avro</groupId>
+  <artifactId>avro-registry-adapter-default</artifactId>
+  <description>Implementation of serialization using Avro library defaults and useful defaults for the adapter API.</description>
+
+  <properties>
+    <jacoco.skip>false</jacoco.skip>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>io.holixon.avro</groupId>
+      <artifactId>avro-registry-adapter-api</artifactId>
+    </dependency>
+  </dependencies>
+
+</project>
diff --git a/extension/default/src/main/kotlin/AvroAdapterDefault.kt b/extension/default/src/main/kotlin/AvroAdapterDefault.kt
new file mode 100644
index 00000000..23fb613c
--- /dev/null
+++ b/extension/default/src/main/kotlin/AvroAdapterDefault.kt
@@ -0,0 +1,124 @@
+package io.holixon.avro.adapter.common
+
+import io.holixon.avro.adapter.api.AvroPayloadAndSchemaId
+import io.holixon.avro.adapter.api.AvroSchemaWithId
+import io.holixon.avro.adapter.api.AvroSingleObjectEncoded
+import io.holixon.avro.adapter.api.SchemaIdSupplier
+import io.holixon.avro.adapter.api.ext.ByteArrayExt.buffer
+import io.holixon.avro.adapter.api.ext.ByteArrayExt.extract
+import io.holixon.avro.adapter.api.ext.ByteArrayExt.split
+import io.holixon.avro.adapter.api.ext.ByteArrayExt.toHexString
+import io.holixon.avro.adapter.api.type.AvroPayloadAndSchemaIdData
+import io.holixon.avro.adapter.common.AvroAdapterDefault.DecoderSpecificRecordClassResolver
+import io.holixon.avro.adapter.common.converter.DefaultSchemaCompatibilityResolver
+import io.holixon.avro.adapter.common.registry.InMemoryAvroSchemaRegistry
+import org.apache.avro.Schema
+import org.apache.avro.SchemaNormalization
+import org.apache.avro.specific.SpecificRecordBase
+import org.apache.avro.util.ClassUtils
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+import java.util.function.Function
+
+/**
+ * Collection of default adapter helper functions.
+ */
+object AvroAdapterDefault {
+
+  const val PROPERTY_REVISION = "revision"
+
+  /**
+   * Marker bytes according to Avro schema specification v1.
+   */
+  @JvmField
+  val AVRO_V1_HEADER = byteArrayOf(-61, 1) // [C3 01]
+  private val AVRO_HEADER_LENGTH = AVRO_V1_HEADER.size + Long.SIZE_BYTES
+
+  /**
+   * Read payload and schema id from Avro single object encoded ball.
+   */
+  fun AvroSingleObjectEncoded.readPayloadAndSchemaId(): AvroPayloadAndSchemaId {
+    require(this.size > AVRO_HEADER_LENGTH) { "Single object encoded bytes must have at least length > $AVRO_HEADER_LENGTH, was: $size." }
+    require(this.isAvroSingleObjectEncoded()) { "Single object encoded bytes need to start with ${AVRO_V1_HEADER.toHexString()}." }
+
+    val (_, idBytes, payloadBytes) = this.split(AVRO_V1_HEADER.size, AVRO_HEADER_LENGTH)
+
+    return AvroPayloadAndSchemaIdData(
+      schemaId = "${idBytes.readLong()}",
+      payload = payloadBytes
+    )
+  }
+
+  private fun ByteArray.readLong(): Long {
+    require(this.size == Long.SIZE_BYTES) { "Size must be exactly Long.SIZE_BYTES (${Long.SIZE_BYTES}." }
+    return this.buffer().order(ByteOrder.LITTLE_ENDIAN).long
+  }
+
+  @JvmStatic
+  fun ByteBuffer.isAvroSingleObjectEncoded(): Boolean = extract(0, AVRO_V1_HEADER.size).contentEquals(AVRO_V1_HEADER)
+
+  @JvmStatic
+  fun ByteArray.isAvroSingleObjectEncoded(): Boolean = buffer().isAvroSingleObjectEncoded()
+
+  @JvmField
+  val schemaRevisionResolver = DefaultSchemaRevisionResolver()
+
+  /**
+   * Implements [SchemaIdSupplier] by using SchemaNormalization#parsingFingerprint64(Schema).
+   */
+  @JvmField
+  val schemaIdSupplier = DefaultSchemaIdSupplier()
+
+  /**
+   * Create a in-memory schema registry using [SchemaNormalization.parsingFingerprint64] and [DefaultSchemaRevisionResolver].
+   */
+  fun inMemorySchemaRegistry() = InMemoryAvroSchemaRegistry(
+    schemaIdSupplier = schemaIdSupplier,
+    schemaRevisionResolver = schemaRevisionResolver
+  )
+
+  /**
+   * Reflective access using the method of generated specific record to access byte buffer representation.
+   */
+  @JvmStatic
+  fun SpecificRecordBase.toByteBuffer(): ByteBuffer = this.javaClass.getDeclaredMethod("toByteBuffer").invoke(this) as ByteBuffer
+
+  /**
+   * Reflective access using the method of generated specific record to access byte array representation.
+   * @see toByteBuffer
+   */
+  @JvmStatic
+  fun SpecificRecordBase.toByteArray(): ByteArray = this.toByteBuffer().array()
+
+  /**
+   * Reflective access using the method of generated specific record to access specific record base representation.
+   */
+  @JvmStatic
+  fun Class<SpecificRecordBase>.fromByteArray(bytes: ByteArray) = this.getDeclaredMethod("fromByteBuffer", ByteBuffer::class.java)
+    .invoke(null, ByteBuffer.wrap(bytes)) as SpecificRecordBase
+
+  /**
+   * Retrieves the schema from generated class extending specific record class.
+   */
+  @JvmStatic
+  fun Class<SpecificRecordBase>.getSchema() = this.getDeclaredField("SCHEMA$").get(null) as Schema
+
+  /**
+   * Resolver for a concrete class used by decoding of Avro single object into Avro specific record.
+   */
+  fun interface DecoderSpecificRecordClassResolver : Function<AvroSchemaWithId, Class<SpecificRecordBase>>
+
+  /**
+   * Default implementation using [Class.forName].
+   */
+  @Suppress("UNCHECKED_CAST")
+  val reflectionBasedDecoderSpecificRecordClassResolver = DecoderSpecificRecordClassResolver {
+    ClassUtils.forName(it.canonicalName) as Class<SpecificRecordBase>
+  }
+
+  /**
+   * Default schema compatibility resolver throwing exception on any incompatibility.
+   */
+  @JvmField
+  val defaultSchemaCompatibilityResolver = DefaultSchemaCompatibilityResolver()
+}
diff --git a/extension/default/src/main/kotlin/DefaultSchemaIdSupplier.kt b/extension/default/src/main/kotlin/DefaultSchemaIdSupplier.kt
new file mode 100644
index 00000000..f69c8147
--- /dev/null
+++ b/extension/default/src/main/kotlin/DefaultSchemaIdSupplier.kt
@@ -0,0 +1,13 @@
+package io.holixon.avro.adapter.common
+
+import io.holixon.avro.adapter.api.AvroSchemaId
+import io.holixon.avro.adapter.api.SchemaIdSupplier
+import io.holixon.avro.adapter.common.ext.SchemaExt.fingerprint
+import org.apache.avro.Schema
+
+/**
+ * Delivers schema id based on fingerprint.
+ */
+class DefaultSchemaIdSupplier : SchemaIdSupplier {
+  override fun apply(schema: Schema): AvroSchemaId = schema.fingerprint.toString()
+}
diff --git a/extension/default/src/main/kotlin/DefaultSchemaRevisionResolver.kt b/extension/default/src/main/kotlin/DefaultSchemaRevisionResolver.kt
new file mode 100644
index 00000000..fa345418
--- /dev/null
+++ b/extension/default/src/main/kotlin/DefaultSchemaRevisionResolver.kt
@@ -0,0 +1,17 @@
+package io.holixon.avro.adapter.common
+
+import io.holixon.avro.adapter.api.AvroAdapterApi
+import io.holixon.avro.adapter.api.AvroSchemaRevision
+import io.holixon.avro.adapter.api.SchemaRevisionResolver
+import org.apache.avro.Schema
+import java.util.*
+
+/**
+ * Default schema revision resolver based on a class property.
+ */
+class DefaultSchemaRevisionResolver : SchemaRevisionResolver {
+  private val propertyBasedResolver = AvroAdapterApi.propertyBasedSchemaRevisionResolver(AvroAdapterDefault.PROPERTY_REVISION)
+
+  override fun apply(schema: Schema): Optional<AvroSchemaRevision> = propertyBasedResolver.apply(schema)
+
+}
diff --git a/extension/default/src/main/kotlin/DefaultSchemaStore.kt b/extension/default/src/main/kotlin/DefaultSchemaStore.kt
new file mode 100644
index 00000000..847d7f52
--- /dev/null
+++ b/extension/default/src/main/kotlin/DefaultSchemaStore.kt
@@ -0,0 +1,16 @@
+package io.holixon.avro.adapter.common
+
+import io.holixon.avro.adapter.api.SchemaResolver
+import org.apache.avro.Schema
+import org.apache.avro.message.SchemaStore
+
+/**
+ * Schema store using the schema resolver.
+ */
+class DefaultSchemaStore(private val schemaResolver: SchemaResolver) : SchemaStore {
+
+  override fun findByFingerprint(fingerprint: Long): Schema? = schemaResolver
+    .apply(fingerprint.toString())
+    .map { it.schema }
+    .orElse(null)
+}
diff --git a/extension/default/src/main/kotlin/converter/DefaultSchemaCompatibilityResolver.kt b/extension/default/src/main/kotlin/converter/DefaultSchemaCompatibilityResolver.kt
new file mode 100644
index 00000000..7b9a0ff5
--- /dev/null
+++ b/extension/default/src/main/kotlin/converter/DefaultSchemaCompatibilityResolver.kt
@@ -0,0 +1,47 @@
+package io.holixon.avro.adapter.common.converter
+
+import io.holixon.avro.adapter.api.AvroSchemaIncompatibilityResolver
+import io.holixon.avro.adapter.api.ext.FunctionalExt.invoke
+import io.holixon.avro.adapter.common.AvroAdapterDefault.schemaIdSupplier
+import org.apache.avro.Schema
+import org.apache.avro.SchemaCompatibility
+import org.apache.avro.SchemaCompatibility.SchemaCompatibilityType.*
+
+/**
+ * Detects and possibly resolves schema incompatibilities.
+ */
+class DefaultSchemaCompatibilityResolver @JvmOverloads constructor(
+  val ignoredIncompatibilityTypes: Set<SchemaCompatibility.SchemaIncompatibilityType> = setOf()
+) : AvroSchemaIncompatibilityResolver {
+
+  override fun resolve(readerSchema: Schema, writerSchema: Schema): Schema {
+
+    // check compatibility
+    val compatibility = SchemaCompatibility.checkReaderWriterCompatibility(readerSchema, writerSchema)
+    return when (compatibility.result.compatibility!!) {
+      COMPATIBLE -> readerSchema
+      INCOMPATIBLE -> if (filterIgnored(compatibility.result.incompatibilities.map { it.type }).isEmpty()) {
+        readerSchema
+      } else {
+        throw IllegalArgumentException(
+          "Reader schema[${readerSchema.schemaId}] is not compatible with Writer schema[${writerSchema.schemaId}]. The incompatibilities are: ${
+            filterIgnored(
+              compatibility.result.incompatibilities.map { it.type })
+          }"
+        )
+      }
+      RECURSION_IN_PROGRESS -> throw IllegalArgumentException(
+        "Recursion in progress for compatibility check for Reader schema[${readerSchema.schemaId}] and Writer schema[${writerSchema.schemaId}]."
+      )
+    }
+  }
+
+  private val Schema.schemaId get() = schemaIdSupplier(this)
+
+  /**
+   * Filters found incompatibilities by ignoring the types provided in [ignoredIncompatibilityTypes].
+   */
+  fun filterIgnored(incompatibilities: List<SchemaCompatibility.SchemaIncompatibilityType>) = (incompatibilities - ignoredIncompatibilityTypes)
+
+}
+
diff --git a/extension/default/src/main/kotlin/converter/DefaultSpecificRecordToSingleObjectConverter.kt b/extension/default/src/main/kotlin/converter/DefaultSpecificRecordToSingleObjectConverter.kt
new file mode 100644
index 00000000..690eee27
--- /dev/null
+++ b/extension/default/src/main/kotlin/converter/DefaultSpecificRecordToSingleObjectConverter.kt
@@ -0,0 +1,52 @@
+package io.holixon.avro.adapter.common.converter
+
+import io.holixon.avro.adapter.api.AvroSchemaIncompatibilityResolver
+import io.holixon.avro.adapter.api.AvroSingleObjectEncoded
+import io.holixon.avro.adapter.api.SchemaResolver
+import io.holixon.avro.adapter.api.converter.SpecificRecordToSingleObjectConverter
+import io.holixon.avro.adapter.common.AvroAdapterDefault
+import io.holixon.avro.adapter.common.AvroAdapterDefault.DecoderSpecificRecordClassResolver
+import io.holixon.avro.adapter.common.AvroAdapterDefault.getSchema
+import io.holixon.avro.adapter.common.AvroAdapterDefault.readPayloadAndSchemaId
+import io.holixon.avro.adapter.common.AvroAdapterDefault.toByteArray
+import io.holixon.avro.adapter.common.DefaultSchemaStore
+import org.apache.avro.message.BinaryMessageDecoder
+import org.apache.avro.specific.SpecificData
+import org.apache.avro.specific.SpecificRecordBase
+
+/**
+ * Converts any instance derived from [SpecificRecordBase] (generated from avsc) to a [ByteArray] that follows the format specified
+ * in the [avro specs](https://avro.apache.org/docs/current/spec.html#single_object_encoding).
+ */
+class DefaultSpecificRecordToSingleObjectConverter @JvmOverloads constructor(
+  private val schemaResolver: SchemaResolver,
+  private val decoderSpecificRecordClassResolver: DecoderSpecificRecordClassResolver = AvroAdapterDefault.reflectionBasedDecoderSpecificRecordClassResolver,
+  private val schemaIncompatibilityResolver: AvroSchemaIncompatibilityResolver = AvroAdapterDefault.defaultSchemaCompatibilityResolver
+) : SpecificRecordToSingleObjectConverter {
+
+  private val schemaStore = DefaultSchemaStore(schemaResolver)
+
+  override fun <T : SpecificRecordBase> encode(data: T): AvroSingleObjectEncoded = data.toByteArray()
+
+
+  override fun <T : SpecificRecordBase> decode(bytes: AvroSingleObjectEncoded): T {
+
+    // get the reader schema id from the single object encoded bytes
+    val schemaId = bytes.readPayloadAndSchemaId().schemaId
+    // load writer schema info from schema resolver
+    val writerSchemaWithId =
+      schemaResolver.apply(schemaId).orElseThrow { IllegalArgumentException("Can not resolve writer schema for id=$schemaId.") }
+    // we have to assume that the namespace and name of the message payload did not change, so we try to load the class based on the schema info
+    // of the writer schema. This might lead to another (earlier or later) revision, but the canonical name should not have changed.
+    val targetClass: Class<SpecificRecordBase> = decoderSpecificRecordClassResolver.apply(writerSchemaWithId)
+    // get reader schema from the class
+    val readerSchema = targetClass.getSchema()
+
+    // resolve incompatibilities if any and return the resulting reader schema
+    val resolvedReaderSchema = schemaIncompatibilityResolver.resolve(readerSchema, writerSchemaWithId.schema)
+
+    // construct decoder and decode
+    return BinaryMessageDecoder<T>(SpecificData(), resolvedReaderSchema, schemaStore).decode(bytes)
+  }
+}
+
diff --git a/extension/default/src/main/kotlin/converter/SingleObjectToJson.kt b/extension/default/src/main/kotlin/converter/SingleObjectToJson.kt
new file mode 100644
index 00000000..2f36b7b6
--- /dev/null
+++ b/extension/default/src/main/kotlin/converter/SingleObjectToJson.kt
@@ -0,0 +1,42 @@
+package io.holixon.avro.adapter.common.converter
+
+import io.holixon.avro.adapter.api.AvroSingleObjectEncoded
+import io.holixon.avro.adapter.api.SchemaResolver
+import io.holixon.avro.adapter.common.AvroAdapterDefault.readPayloadAndSchemaId
+import org.apache.avro.Schema
+import org.apache.avro.generic.GenericDatumReader
+import org.apache.avro.generic.GenericDatumWriter
+import org.apache.avro.io.*
+import java.io.ByteArrayOutputStream
+import java.nio.charset.StandardCharsets
+import java.util.function.Function
+
+/**
+ * Uses the schema encoded in the single object bytes to create a generic datum and convert it to json.
+ */
+class SingleObjectToJson(private val schemaResolver: SchemaResolver) : Function<ByteArray, String> {
+
+  override fun apply(avroSingleObject: AvroSingleObjectEncoded): String {
+    val (schemaId, payload) = avroSingleObject.readPayloadAndSchemaId()
+    val writerSchema = schemaResolver.apply(schemaId).orElseThrow().schema
+
+    val genericDatum = readGenericDatum(writerSchema, payload)
+
+    return writeToJson(writerSchema, genericDatum)
+  }
+
+  private fun writeToJson(schema: Schema, genericDatum: Any): String = ByteArrayOutputStream().use {
+    val writer: DatumWriter<Any> = GenericDatumWriter(schema)
+    val encoder = EncoderFactory.get().jsonEncoder(schema, it, false)
+    writer.write(genericDatum, encoder)
+    encoder.flush()
+    it.flush()
+    return String(it.toByteArray(), StandardCharsets.UTF_8)
+  }
+
+  private fun readGenericDatum(schema: Schema, avroBinary: ByteArray): Any {
+    val datumReader: DatumReader<Any> = GenericDatumReader(schema)
+    val decoder: Decoder = DecoderFactory.get().binaryDecoder(avroBinary, null)
+    return datumReader.read(null, decoder)
+  }
+}
diff --git a/extension/default/src/main/kotlin/ext/SchemaExt.kt b/extension/default/src/main/kotlin/ext/SchemaExt.kt
new file mode 100644
index 00000000..1bf008d8
--- /dev/null
+++ b/extension/default/src/main/kotlin/ext/SchemaExt.kt
@@ -0,0 +1,13 @@
+package io.holixon.avro.adapter.common.ext
+
+import org.apache.avro.Schema
+import org.apache.avro.SchemaNormalization
+
+/**
+ * Extension functions to retrieve schema fingerprint.
+ */
+object SchemaExt {
+
+  val Schema.fingerprint get() = SchemaNormalization.parsingFingerprint64(this)
+
+}
diff --git a/extension/default/src/main/kotlin/registry/CompositeAvroSchemaReadOnlyRegistry.kt b/extension/default/src/main/kotlin/registry/CompositeAvroSchemaReadOnlyRegistry.kt
new file mode 100644
index 00000000..14b1fb9a
--- /dev/null
+++ b/extension/default/src/main/kotlin/registry/CompositeAvroSchemaReadOnlyRegistry.kt
@@ -0,0 +1,66 @@
+package io.holixon.avro.adapter.common.registry
+
+import io.holixon.avro.adapter.api.*
+import java.util.*
+
+/**
+ * Composite Avro registry allowing registry composition.
+ */
+class CompositeAvroSchemaReadOnlyRegistry(
+  private val registries: List<AvroSchemaReadOnlyRegistry>
+) : AvroSchemaReadOnlyRegistry {
+
+  /**
+   * Using registries or read-only registries as varargs.
+   */
+  constructor(vararg registry: AvroSchemaRegistry) : this(registry.asList())
+
+  /**
+   * Using registries or read-only registries as varargs.
+   */
+  constructor(vararg registry: AvroSchemaReadOnlyRegistry) : this(registry.asList())
+
+  init {
+    require(registries.isNotEmpty()) { "Composite Avro Schema Registry must contain at least one registry." }
+  }
+
+  override fun findById(schemaId: AvroSchemaId): Optional<AvroSchemaWithId> {
+    for (registry in registries) {
+      val result = registry.findById(schemaId)
+      if (result.isPresent) {
+        return result
+      }
+    }
+    return Optional.empty()
+  }
+
+  override fun findByInfo(info: AvroSchemaInfo): Optional<AvroSchemaWithId> {
+    for (registry in registries) {
+      val result = registry.findByInfo(info)
+      if (result.isPresent) {
+        return result
+      }
+    }
+    return Optional.empty()
+  }
+
+  override fun findAllByCanonicalName(namespace: String, name: String): List<AvroSchemaWithId> {
+    for (registry in registries) {
+      val result = registry.findAllByCanonicalName(namespace, name)
+      if (result.isNotEmpty()) {
+        return result
+      }
+    }
+    return listOf()
+  }
+
+  override fun findAll(): List<AvroSchemaWithId> {
+    for (registry in registries) {
+      val result = registry.findAll()
+      if (result.isNotEmpty()) {
+        return result
+      }
+    }
+    return listOf()
+  }
+}
diff --git a/extension/default/src/main/kotlin/registry/CompositeAvroSchemaRegistry.kt b/extension/default/src/main/kotlin/registry/CompositeAvroSchemaRegistry.kt
new file mode 100644
index 00000000..3ca90cdf
--- /dev/null
+++ b/extension/default/src/main/kotlin/registry/CompositeAvroSchemaRegistry.kt
@@ -0,0 +1,42 @@
+package io.holixon.avro.adapter.common.registry
+
+import io.holixon.avro.adapter.api.AvroSchemaReadOnlyRegistry
+import io.holixon.avro.adapter.api.AvroSchemaRegistry
+import io.holixon.avro.adapter.api.AvroSchemaWithId
+import org.apache.avro.Schema
+
+/**
+ * Composite registry delegating the registration to a dedicated registry and using a composite read only registry for schema retrieval.
+ */
+class CompositeAvroSchemaRegistry(
+  private val registry: AvroSchemaRegistry,
+  private val compositeAvroSchemaReadOnlyRegistry: CompositeAvroSchemaReadOnlyRegistry
+) : AvroSchemaReadOnlyRegistry by compositeAvroSchemaReadOnlyRegistry, AvroSchemaRegistry {
+
+  /**
+   * Convenience constructor for a composite registry.
+   * @param readOnlyRegistries list of read-only registries used for finding schemas.
+   * @param registry registry used for schema registration.
+   */
+  constructor(registry: AvroSchemaRegistry, vararg readOnlyRegistries: AvroSchemaReadOnlyRegistry) : this(
+    registry,
+    readOnlyRegistries.toList()
+  )
+
+  /**
+   * Convenience constructor for a composite registry.
+   * @param readOnlyRegistries list of read-only registries used for finding schemas.
+   * @param registry registry used for schema registration.
+   */
+  constructor(registry: AvroSchemaRegistry, readOnlyRegistries: List<AvroSchemaReadOnlyRegistry>) : this(
+    registry,
+    CompositeAvroSchemaReadOnlyRegistry(readOnlyRegistries)
+  )
+
+  /*
+   * Delegate registration to the registry.
+   */
+  override fun register(schema: Schema): AvroSchemaWithId {
+    return registry.register(schema)
+  }
+}
diff --git a/extension/default/src/main/kotlin/registry/InMemoryAvroSchemaRegistry.kt b/extension/default/src/main/kotlin/registry/InMemoryAvroSchemaRegistry.kt
new file mode 100644
index 00000000..1d019174
--- /dev/null
+++ b/extension/default/src/main/kotlin/registry/InMemoryAvroSchemaRegistry.kt
@@ -0,0 +1,58 @@
+package io.holixon.avro.adapter.common.registry
+
+import io.holixon.avro.adapter.api.*
+import io.holixon.avro.adapter.api.AvroAdapterApi.extractSchemaInfo
+import io.holixon.avro.adapter.api.type.AvroSchemaWithIdData
+import org.apache.avro.Schema
+import java.util.*
+import java.util.concurrent.ConcurrentHashMap
+
+/**
+ * An implementation of [AvroSchemaRegistry] that does not use persistent storage.
+ * Obviously, this is primarily meant for testing and should not be used for real life projects.
+ */
+class InMemoryAvroSchemaRegistry(
+  private val store: ConcurrentHashMap<String, Pair<AvroSchemaInfo, Schema>>,
+  val schemaIdSupplier: SchemaIdSupplier,
+  val schemaRevisionResolver: SchemaRevisionResolver
+) : AvroSchemaRegistry, AutoCloseable {
+
+  /**
+   * Create instance with default empty [ConcurrentHashMap] as store.
+   */
+  constructor(schemaIdSupplier: SchemaIdSupplier, schemaRevisionResolver: SchemaRevisionResolver) : this(ConcurrentHashMap(), schemaIdSupplier, schemaRevisionResolver)
+
+  override fun register(schema: Schema): AvroSchemaWithId {
+    val info = schema.extractSchemaInfo(schemaRevisionResolver)
+
+    return findByInfo(info)
+      .orElseGet {
+        val id = schemaIdSupplier.apply(schema)
+        store[id] = info to schema
+        findById(id).get()
+      }
+  }
+
+  override fun findById(schemaId: AvroSchemaId): Optional<AvroSchemaWithId> = Optional.ofNullable(
+    store[schemaId]
+  ).map { AvroSchemaWithIdData(schemaId, it.second, it.first.revision) }
+
+  override fun findByInfo(info: AvroSchemaInfo): Optional<AvroSchemaWithId> = Optional.ofNullable(store.entries
+    .filter { it.value.first == info }
+    .map { it.toSchemaData() }
+    .firstOrNull())
+
+  override fun findAllByCanonicalName(namespace: String, name: String): List<AvroSchemaWithId> = store
+    .filter { it.value.first.name == name }
+    .filter { it.value.first.namespace == namespace }
+    .map { it.toSchemaData() }
+
+  override fun findAll(): List<AvroSchemaWithId> = store.map { it.toSchemaData() }
+
+  override fun close() {
+    store.clear()
+  }
+
+  private fun Map.Entry<AvroSchemaId, Pair<AvroSchemaInfo, Schema>>.toSchemaData() = AvroSchemaWithIdData(key, value.second)
+
+}
diff --git a/extension/default/src/main/resources/.gitkeep b/extension/default/src/main/resources/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/extension/default/src/test/java/io/holixon/avro/adapter/registry/AvroAdapterApiTest.java b/extension/default/src/test/java/io/holixon/avro/adapter/registry/AvroAdapterApiTest.java
new file mode 100644
index 00000000..4015f310
--- /dev/null
+++ b/extension/default/src/test/java/io/holixon/avro/adapter/registry/AvroAdapterApiTest.java
@@ -0,0 +1,39 @@
+package io.holixon.avro.adapter.registry;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.holixon.avro.adapter.api.AvroAdapterApi;
+import io.holixon.avro.adapter.api.AvroSchemaRegistry;
+import io.holixon.avro.adapter.api.SchemaIdSupplier;
+import io.holixon.avro.adapter.api.SchemaRevisionResolver;
+import io.holixon.avro.adapter.common.registry.InMemoryAvroSchemaRegistry;
+import io.holixon.avro.lib.test.schema.SampleEventV4711;
+import org.apache.avro.Schema;
+import org.apache.avro.SchemaNormalization;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class AvroAdapterApiTest {
+
+  public static final Schema SAMPLE_4711 = SampleEventV4711.INSTANCE.getSchema();
+
+  private static final SchemaRevisionResolver SCHEMA_REVISION_RESOLVER = AvroAdapterApi.propertyBasedSchemaRevisionResolver("revision");
+  private static final SchemaIdSupplier SCHEMA_ID_SUPPLIER = schema -> String.valueOf(SchemaNormalization.parsingFingerprint64(schema));
+
+  private final AvroSchemaRegistry schemaRegistry = new InMemoryAvroSchemaRegistry(SCHEMA_ID_SUPPLIER, SCHEMA_REVISION_RESOLVER);
+
+  @BeforeEach
+  void setUp() {
+    assertThat(schemaRegistry.findAll()).isEmpty();
+  }
+
+  @Test
+  void registerReturnsSchemaWithId() {
+    var registered = schemaRegistry.register(SAMPLE_4711);
+
+    assertThat(schemaRegistry.findAll()).hasSize(1);
+    assertThat(registered.getSchemaId()).isEqualTo(String.valueOf(SampleEventV4711.INSTANCE.getSchemaData().getFingerPrint()));
+  }
+
+
+}
diff --git a/extension/default/src/test/kotlin/AvroAdapterApiTestHelper.kt b/extension/default/src/test/kotlin/AvroAdapterApiTestHelper.kt
new file mode 100644
index 00000000..99061e51
--- /dev/null
+++ b/extension/default/src/test/kotlin/AvroAdapterApiTestHelper.kt
@@ -0,0 +1,15 @@
+package io.holixon.avro.adapter.common
+
+import io.holixon.avro.adapter.api.AvroAdapterApi
+import io.holixon.avro.adapter.api.SchemaIdSupplier
+import org.apache.avro.SchemaNormalization
+
+/**
+ * Test helper.
+ */
+object AvroAdapterApiTestHelper {
+
+  val schemaIdSupplier = SchemaIdSupplier { schema -> SchemaNormalization.parsingFingerprint64(schema).toString() }
+
+  val schemaRevisionResolver = AvroAdapterApi.propertyBasedSchemaRevisionResolver("revision")
+}
diff --git a/extension/default/src/test/kotlin/AvroAdapterDefaultTest.kt b/extension/default/src/test/kotlin/AvroAdapterDefaultTest.kt
new file mode 100644
index 00000000..da9e6ba9
--- /dev/null
+++ b/extension/default/src/test/kotlin/AvroAdapterDefaultTest.kt
@@ -0,0 +1,53 @@
+package io.holixon.avro.adapter.common
+
+import io.holixon.avro.adapter.api.ext.ByteArrayExt.toHexString
+import io.holixon.avro.adapter.common.AvroAdapterDefault.isAvroSingleObjectEncoded
+import io.holixon.avro.adapter.common.AvroAdapterDefault.readPayloadAndSchemaId
+import io.holixon.avro.adapter.common.AvroAdapterDefault.toByteArray
+import io.holixon.avro.adapter.common.ext.SchemaExt.fingerprint
+import io.holixon.avro.lib.test.AvroAdapterTestLib
+import mu.KLogging
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.assertThatThrownBy
+import org.junit.jupiter.api.Test
+
+internal class AvroAdapterDefaultTest {
+  companion object : KLogging()
+
+  private val bytes = AvroAdapterTestLib.sampleFoo.toByteArray()
+
+  @Test
+  internal fun `read payload and schemaId from encoded bytes`() {
+    logger.info { bytes.toHexString() }
+
+    // too short
+    assertThatThrownBy { "foo".encodeToByteArray().readPayloadAndSchemaId() }.isInstanceOf(IllegalArgumentException::class.java)
+      .hasMessage("Single object encoded bytes must have at least length > 10, was: 3.")
+
+    // long enough, but does not start with V1_Header
+    assertThatThrownBy {
+      "hello my precious world".encodeToByteArray().readPayloadAndSchemaId()
+    }.isInstanceOf(IllegalArgumentException::class.java)
+      .hasMessage("Single object encoded bytes need to start with [C3 01].")
+
+    val payloadAndSchemaId = bytes.readPayloadAndSchemaId()
+    assertThat(payloadAndSchemaId.schemaId).isEqualTo(AvroAdapterTestLib.sampleFoo.schema.fingerprint.toString())
+    assertThat(payloadAndSchemaId.payload.toHexString()).isEqualTo("[06 66 6F 6F]")
+  }
+
+  @Test
+  internal fun `extract schemaId and payload`() {
+    val (schemaId, payload) = bytes.readPayloadAndSchemaId()
+
+    assertThat(schemaId).isEqualTo(AvroAdapterTestLib.sampleEventFingerprint.toString())
+    assertThat(payload.toHexString()).isEqualTo("[06 66 6F 6F]")
+  }
+
+  @Test
+  internal fun `is avro single object encoded`() {
+    assertThat(bytes.isAvroSingleObjectEncoded()).isTrue
+
+    assertThat("foo".encodeToByteArray().isAvroSingleObjectEncoded()).isFalse
+  }
+
+}
diff --git a/extension/default/src/test/kotlin/converter/DefaultSchemaCompatibilityResolverTest.kt b/extension/default/src/test/kotlin/converter/DefaultSchemaCompatibilityResolverTest.kt
new file mode 100644
index 00000000..db85e904
--- /dev/null
+++ b/extension/default/src/test/kotlin/converter/DefaultSchemaCompatibilityResolverTest.kt
@@ -0,0 +1,35 @@
+package io.holixon.avro.adapter.common.converter
+
+import org.apache.avro.SchemaCompatibility
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+
+internal class DefaultSchemaCompatibilityResolverTest {
+
+
+  @Test
+  fun `check ignored incompatibilities empty`() {
+    val converter = DefaultSchemaCompatibilityResolver()
+    assertThat(
+      converter.filterIgnored(listOf(SchemaCompatibility.SchemaIncompatibilityType.NAME_MISMATCH))
+    ).isNotEmpty
+  }
+
+  @Test
+  fun `check ignored incompatibilities are matched`() {
+    val converter = DefaultSchemaCompatibilityResolver(setOf(SchemaCompatibility.SchemaIncompatibilityType.NAME_MISMATCH))
+    assertThat(
+      converter.filterIgnored(listOf(SchemaCompatibility.SchemaIncompatibilityType.NAME_MISMATCH))
+    ).isEmpty()
+  }
+
+
+  @Test
+  fun `check empty incompatibilities are ok`() {
+    val converter = DefaultSchemaCompatibilityResolver(setOf(SchemaCompatibility.SchemaIncompatibilityType.NAME_MISMATCH))
+    assertThat(
+      converter.filterIgnored(listOf())
+    ).isEmpty()
+  }
+
+}
diff --git a/extension/default/src/test/kotlin/converter/DefaultSpecificRecordToSingleObjectConverterTest.kt b/extension/default/src/test/kotlin/converter/DefaultSpecificRecordToSingleObjectConverterTest.kt
new file mode 100644
index 00000000..99f3caa7
--- /dev/null
+++ b/extension/default/src/test/kotlin/converter/DefaultSpecificRecordToSingleObjectConverterTest.kt
@@ -0,0 +1,109 @@
+package io.holixon.avro.adapter.common.converter
+
+import io.holixon.avro.adapter.api.AvroAdapterApi.schemaResolver
+import io.holixon.avro.adapter.common.AvroAdapterDefault
+import io.holixon.avro.adapter.common.AvroAdapterDefault.DecoderSpecificRecordClassResolver
+import io.holixon.avro.adapter.common.AvroAdapterDefault.reflectionBasedDecoderSpecificRecordClassResolver
+import io.holixon.avro.lib.test.AvroAdapterTestLib
+import io.holixon.avro.lib.test.schema.SampleEventV4713
+import org.apache.avro.SchemaCompatibility
+import org.apache.avro.SchemaCompatibility.SchemaIncompatibilityType.NAME_MISMATCH
+import org.apache.avro.specific.SpecificRecordBase
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.assertThatThrownBy
+import org.junit.jupiter.api.Test
+import test.fixture.SampleEvent
+import test.fixture.SampleEventWithAdditionalFieldWithDefault
+
+/**
+ * Test for converter.
+ */
+internal class DefaultSpecificRecordToSingleObjectConverterTest {
+
+  private val registry = AvroAdapterDefault.inMemorySchemaRegistry()
+
+  @Test
+  internal fun `encode and decode same writer and reader schema`() {
+
+    registry.register(AvroAdapterTestLib.schemaSampleEvent4711)
+    val converter = createConverter()
+
+    val data = AvroAdapterTestLib.sampleFoo
+    val encoded = converter.encode(data)
+
+    val decoded: SampleEvent = converter.decode(encoded)
+
+    assertThat(decoded).isEqualTo(data)
+  }
+
+  @Test
+  internal fun `encode and decode different writer and reader schema`() {
+
+    val reg1 = registry.register(AvroAdapterTestLib.schemaSampleEvent4711)
+    val reg2 = registry.register(AvroAdapterTestLib.schemaSampleEvent4713)
+
+    @Suppress("UNCHECKED_CAST")
+    val converter = createConverter(decoderSpecificRecordClassResolver = {
+      Class.forName(SampleEventV4713.addSuffix(it.canonicalName)) as Class<SpecificRecordBase>
+    })
+
+    val data = AvroAdapterTestLib.sampleFoo
+    val encoded = converter.encode(data)
+
+    assertThatThrownBy {
+      converter.decode<SampleEventWithAdditionalFieldWithDefault>(encoded)
+    }.hasMessage("Reader schema[${reg2.schemaId}] is not compatible with Writer schema[${reg1.schemaId}]. The incompatibilities are: [NAME_MISMATCH]")
+
+  }
+
+
+  @Test
+  internal fun `encode and decode different writer and reader schema ignoring NAME_MISMATCH`() {
+
+    registry.register(AvroAdapterTestLib.schemaSampleEvent4711)
+    registry.register(AvroAdapterTestLib.schemaSampleEvent4713)
+
+    @Suppress("UNCHECKED_CAST")
+    val converter = createConverter(decoderSpecificRecordClassResolver = {
+      Class.forName(SampleEventV4713.addSuffix(it.canonicalName)) as Class<SpecificRecordBase>
+    }, ignoredIncompatibilities = setOf(NAME_MISMATCH))
+
+    val data = AvroAdapterTestLib.sampleFoo
+    val encoded = converter.encode(data)
+
+    val decoded: SampleEventWithAdditionalFieldWithDefault = converter.decode(encoded)
+
+    assertThat(decoded).isEqualTo(AvroAdapterTestLib.sampleFooWithAdditionalFieldWithDefault)
+
+  }
+
+  @Test
+  internal fun `encode and decode different writer=4713 and reader=4711 schema ignoring NAME_MISMATCH`() {
+
+    registry.register(AvroAdapterTestLib.schemaSampleEvent4711)
+    registry.register(AvroAdapterTestLib.schemaSampleEvent4713)
+
+    @Suppress("UNCHECKED_CAST")
+    val converter = createConverter(decoderSpecificRecordClassResolver = {
+      Class.forName(SampleEventV4713.removeSuffix(it.canonicalName)) as Class<SpecificRecordBase>
+    }, ignoredIncompatibilities = setOf(NAME_MISMATCH))
+
+    val data = AvroAdapterTestLib.sampleFooWithAdditionalFieldWithDefault
+    val encoded = converter.encode(data)
+
+    val decoded: SampleEvent = converter.decode(encoded)
+
+    assertThat(decoded).isEqualTo(AvroAdapterTestLib.sampleFoo)
+  }
+
+  private fun createConverter(
+    decoderSpecificRecordClassResolver: DecoderSpecificRecordClassResolver = reflectionBasedDecoderSpecificRecordClassResolver,
+    ignoredIncompatibilities: Set<SchemaCompatibility.SchemaIncompatibilityType> = setOf()
+  ) =
+    DefaultSpecificRecordToSingleObjectConverter(
+      registry.schemaResolver(),
+      decoderSpecificRecordClassResolver,
+      DefaultSchemaCompatibilityResolver(ignoredIncompatibilities)
+    )
+
+}
diff --git a/extension/default/src/test/kotlin/converter/SingleObjectToJsonTest.kt b/extension/default/src/test/kotlin/converter/SingleObjectToJsonTest.kt
new file mode 100644
index 00000000..079e1b40
--- /dev/null
+++ b/extension/default/src/test/kotlin/converter/SingleObjectToJsonTest.kt
@@ -0,0 +1,28 @@
+package io.holixon.avro.adapter.common.converter
+
+import io.holixon.avro.adapter.api.AvroAdapterApi.schemaResolver
+import io.holixon.avro.adapter.common.AvroAdapterDefault
+import io.holixon.avro.adapter.common.AvroAdapterDefault.toByteArray
+import io.holixon.avro.lib.test.AvroAdapterTestLib
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+
+
+internal class SingleObjectToJsonTest {
+
+  private val sample = AvroAdapterTestLib.sampleFoo
+
+  private val registry = AvroAdapterDefault.inMemorySchemaRegistry().apply {
+    register(sample.schema)
+  }
+
+  private val bytes = sample.toByteArray()
+  private val expectedJson = sample.toString().replace("\\s".toRegex(), "")
+
+  private val fn = SingleObjectToJson(registry.schemaResolver())
+
+  @Test
+  internal fun `convert bytes to json`() {
+    assertThat(fn.apply(bytes)).isEqualTo(expectedJson)
+  }
+}
diff --git a/extension/default/src/test/kotlin/registry/CompositeRegistryTest.kt b/extension/default/src/test/kotlin/registry/CompositeRegistryTest.kt
new file mode 100644
index 00000000..e80c4de2
--- /dev/null
+++ b/extension/default/src/test/kotlin/registry/CompositeRegistryTest.kt
@@ -0,0 +1,140 @@
+package io.holixon.avro.adapter.common.registry
+
+import io.holixon.avro.adapter.api.AvroSchemaReadOnlyRegistry
+import io.holixon.avro.adapter.api.AvroSchemaRegistry
+import io.holixon.avro.adapter.api.AvroSchemaWithId
+import io.holixon.avro.adapter.api.type.AvroSchemaInfoData
+import io.holixon.avro.adapter.api.type.AvroSchemaWithIdData
+import io.holixon.avro.adapter.common.AvroAdapterApiTestHelper
+import io.holixon.avro.lib.test.schema.SampleEventV4711
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.mockito.kotlin.*
+import java.util.*
+
+internal class CompositeRegistryTest {
+
+
+  private val schema4711 =
+    AvroSchemaWithIdData(AvroAdapterApiTestHelper.schemaIdSupplier.apply(SampleEventV4711.schema), SampleEventV4711.schema)
+
+  private val fastRoRegistry: AvroSchemaReadOnlyRegistry = mock()
+  private val mediumRwRegistry: AvroSchemaRegistry = mock()
+  private val slowRwRegistry: AvroSchemaRegistry = mock()
+
+  private val registry: AvroSchemaRegistry = CompositeAvroSchemaRegistry(slowRwRegistry, fastRoRegistry, mediumRwRegistry, slowRwRegistry)
+
+  @Test
+  fun `should deliver from the first registry if found`() {
+    mockRegistryFindMethods(fastRoRegistry, schema4711)
+    mockRegistryFindMethods(mediumRwRegistry, null)
+    mockRegistryFindMethods(slowRwRegistry, null)
+
+    callRegistryFindMethodsAndFindResult()
+
+    verifyCallsExecuted(fastRoRegistry, true)
+    verifyCallsExecuted(mediumRwRegistry, false)
+    verifyCallsExecuted(slowRwRegistry, false)
+  }
+
+  @Test
+  fun `should deliver from the second registry if not found in the first`() {
+    mockRegistryFindMethods(fastRoRegistry, null)
+    mockRegistryFindMethods(mediumRwRegistry, schema4711)
+    mockRegistryFindMethods(slowRwRegistry, null)
+
+    callRegistryFindMethodsAndFindResult()
+
+    verifyCallsExecuted(fastRoRegistry, true)
+    verifyCallsExecuted(mediumRwRegistry, true)
+    verifyCallsExecuted(slowRwRegistry, false)
+  }
+
+  @Test
+  fun `should deliver from the third registry if not found in the first and second`() {
+    mockRegistryFindMethods(fastRoRegistry, null)
+    mockRegistryFindMethods(mediumRwRegistry, null)
+    mockRegistryFindMethods(slowRwRegistry, schema4711)
+
+    callRegistryFindMethodsAndFindResult()
+
+    verifyCallsExecuted(fastRoRegistry, true)
+    verifyCallsExecuted(mediumRwRegistry, true)
+    verifyCallsExecuted(slowRwRegistry, true)
+  }
+
+  @Test
+  fun `should ask all registries if not found`() {
+    mockRegistryFindMethods(fastRoRegistry, null)
+    mockRegistryFindMethods(mediumRwRegistry, null)
+    mockRegistryFindMethods(slowRwRegistry, null)
+
+    callRegistryFindMethodsAndDontFindResult()
+
+    verifyCallsExecuted(fastRoRegistry, true)
+    verifyCallsExecuted(mediumRwRegistry, true)
+    verifyCallsExecuted(slowRwRegistry, true)
+  }
+
+  @Test
+  fun `should register schema in the designated registry`() {
+    whenever(slowRwRegistry.register(any())).thenReturn(schema4711)
+
+    registry.register(schema4711.schema)
+
+    verify(slowRwRegistry).register(schema4711.schema)
+    verifyZeroInteractions(mediumRwRegistry)
+  }
+
+  /**
+   * Stubbing of methods.
+   */
+  private fun mockRegistryFindMethods(registry: AvroSchemaReadOnlyRegistry, schemaWithId: AvroSchemaWithId?) {
+    if (schemaWithId != null) {
+      whenever(registry.findById(any())).thenReturn(Optional.of(schemaWithId))
+      whenever(registry.findByInfo(any())).thenReturn(Optional.of(schemaWithId))
+      whenever(registry.findAll()).thenReturn(listOf(schemaWithId))
+      whenever(registry.findAllByCanonicalName(any(), any())).thenReturn(listOf(schemaWithId))
+    } else {
+      whenever(registry.findById(any())).thenReturn(Optional.empty())
+      whenever(registry.findByInfo(any())).thenReturn(Optional.empty())
+      whenever(registry.findAll()).thenReturn(listOf())
+      whenever(registry.findAllByCanonicalName(any(), any())).thenReturn(listOf())
+    }
+  }
+
+  /**
+   * Verification that the call has been (or not) executed.
+   */
+  private fun verifyCallsExecuted(registry: AvroSchemaReadOnlyRegistry, executed: Boolean) {
+    if (executed) {
+      verify(registry).findAll()
+      verify(registry).findAllByCanonicalName(schema4711.namespace, schema4711.name)
+      verify(registry).findById(schema4711.schemaId)
+      verify(registry).findByInfo(AvroSchemaInfoData(schema4711.namespace, schema4711.name, null))
+    } else {
+      verifyNoMoreInteractions(registry)
+    }
+  }
+
+  /**
+   * Actual call of the registry methods.
+   */
+  private fun callRegistryFindMethodsAndFindResult() {
+    assertThat(registry.findAll()).isNotEmpty.containsExactly(schema4711)
+    assertThat(registry.findAllByCanonicalName(schema4711.namespace, schema4711.name)).isNotEmpty.containsExactly(schema4711)
+    assertThat(registry.findById(schema4711.schemaId)).isPresent.get().isEqualTo(schema4711)
+    assertThat(registry.findByInfo(AvroSchemaInfoData(schema4711.namespace, schema4711.name, null))).isPresent.get().isEqualTo(schema4711)
+  }
+
+  /**
+   * Actual call of the registry methods.
+   */
+  private fun callRegistryFindMethodsAndDontFindResult() {
+    assertThat(registry.findAll()).isEmpty()
+    assertThat(registry.findAllByCanonicalName(schema4711.namespace, schema4711.name)).isEmpty()
+    assertThat(registry.findById(schema4711.schemaId)).isEmpty
+    assertThat(registry.findByInfo(AvroSchemaInfoData(schema4711.namespace, schema4711.name, null))).isEmpty
+  }
+
+}
diff --git a/extension/default/src/test/kotlin/registry/InMemoryAvroSchemaRegistryTest.kt b/extension/default/src/test/kotlin/registry/InMemoryAvroSchemaRegistryTest.kt
new file mode 100644
index 00000000..368b0063
--- /dev/null
+++ b/extension/default/src/test/kotlin/registry/InMemoryAvroSchemaRegistryTest.kt
@@ -0,0 +1,58 @@
+package io.holixon.avro.adapter.common.registry
+
+import io.holixon.avro.lib.test.AvroAdapterTestLib
+import io.holixon.avro.adapter.common.AvroAdapterApiTestHelper.schemaIdSupplier
+import io.holixon.avro.adapter.common.AvroAdapterApiTestHelper.schemaRevisionResolver
+import org.apache.avro.Schema
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+
+internal class InMemoryAvroSchemaRegistryTest {
+
+  private val registry = InMemoryAvroSchemaRegistry(
+    schemaIdSupplier = schemaIdSupplier,
+    schemaRevisionResolver = schemaRevisionResolver
+  )
+
+  private val schema = Schema.Parser().parse(AvroAdapterTestLib.loadArvoResource("test.fixture.SampleEvent-v4711"))
+
+  private val schemaId = schemaIdSupplier.apply(schema)
+
+  @BeforeEach
+  internal fun setUp() {
+    assertThat(registry.findAll()).isEmpty()
+  }
+
+  @Test
+  internal fun `register returns schemaWithId`() {
+    val registered = registry.register(schema)
+
+    assertThat(registry.findAll()).hasSize(1)
+    assertThat(registered.schemaId).isEqualTo(schemaId)
+    assertThat(registered.schema).isEqualTo(schema)
+    assertThat(registered.canonicalName).isEqualTo("test.fixture.SampleEvent")
+    assertThat(registered.revision).isEqualTo("4711")
+  }
+
+  @Test
+  internal fun `find by context and name`() {
+    assertThat(registry.findAllByCanonicalName("test.fixture", "SampleEvent")).isEmpty()
+    registry.register(schema)
+
+    assertThat(registry.findAllByCanonicalName("test.fixture", "SampleEvent")).hasSize(1)
+  }
+
+  @Test
+  internal fun `can register schema and find by id`() {
+    registry.register(schema)
+    assertThat(registry.findAll()).isNotEmpty
+
+    val found = registry.findById(schemaId).orElseThrow()
+
+    assertThat(found.schemaId).isEqualTo(schemaId)
+    assertThat(found.schema).isEqualTo(schema)
+    assertThat(found.revision).isEqualTo("4711")
+  }
+
+}
diff --git a/extension/default/src/test/resources/.gitkeep b/extension/default/src/test/resources/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/extension/pom.xml b/extension/pom.xml
new file mode 100644
index 00000000..74a6b910
--- /dev/null
+++ b/extension/pom.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>io.holixon.avro._</groupId>
+    <artifactId>avro-registry-adapter-parent</artifactId>
+    <version>0.0.1</version>
+    <relativePath>../parent/pom.xml</relativePath>
+  </parent>
+
+  <artifactId>extension</artifactId>
+  <packaging>pom</packaging>
+  <description>Parent pom for the extension.</description>
+
+  <modules>
+    <module>api</module>
+    <module>default</module>
+    <module>registry/apicurio-rest</module>
+  </modules>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.jetbrains.kotlin</groupId>
+      <artifactId>kotlin-stdlib-jdk8</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.jetbrains.kotlin</groupId>
+      <artifactId>kotlin-reflect</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.avro</groupId>
+      <artifactId>avro</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>io.holixon.avro._</groupId>
+      <artifactId>avro-registry-adapter-lib-test</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+</project>
diff --git a/extension/registry/apicurio-rest/pom.xml b/extension/registry/apicurio-rest/pom.xml
new file mode 100644
index 00000000..b082441d
--- /dev/null
+++ b/extension/registry/apicurio-rest/pom.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>io.holixon.avro._</groupId>
+    <artifactId>extension</artifactId>
+    <version>0.0.1</version>
+    <relativePath>../../pom.xml</relativePath>
+  </parent>
+
+  <groupId>io.holixon.avro</groupId>
+  <artifactId>avro-registry-adapter-apicurio</artifactId>
+  <description>Adapter implementation using Apicurio Schema registry.</description>
+
+  <properties>
+    <jacoco.skip>false</jacoco.skip>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>io.holixon.avro</groupId>
+      <artifactId>avro-registry-adapter-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.apicurio</groupId>
+      <artifactId>apicurio-registry-client</artifactId>
+      <version>${apicurio.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>io.holixon.avro</groupId>
+      <artifactId>avro-registry-adapter-default</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+  </dependencies>
+</project>
diff --git a/extension/registry/apicurio-rest/src/main/kotlin/ApicurioAvroSchemaRegistry.kt b/extension/registry/apicurio-rest/src/main/kotlin/ApicurioAvroSchemaRegistry.kt
new file mode 100644
index 00000000..7bf02b01
--- /dev/null
+++ b/extension/registry/apicurio-rest/src/main/kotlin/ApicurioAvroSchemaRegistry.kt
@@ -0,0 +1,105 @@
+package io.holixon.avro.adapter.registry.apicurio
+
+import io.apicurio.registry.rest.client.RegistryClient
+import io.apicurio.registry.rest.v2.beans.ArtifactMetaData
+import io.apicurio.registry.rest.v2.beans.EditableMetaData
+import io.apicurio.registry.rest.v2.beans.IfExists
+import io.apicurio.registry.types.ArtifactType
+import io.holixon.avro.adapter.api.*
+import io.holixon.avro.adapter.api.AvroAdapterApi.byteContent
+import io.holixon.avro.adapter.api.type.AvroSchemaWithIdData
+import io.holixon.avro.adapter.registry.apicurio.AvroAdapterApicurioRest.description
+import org.apache.avro.Schema
+import org.slf4j.LoggerFactory
+import java.io.InputStream
+import java.util.*
+
+/**
+ * Implementation of the registry using Apicurio.
+ */
+class ApicurioAvroSchemaRegistry(
+  private val client: RegistryClient,
+  private val schemaIdSupplier: SchemaIdSupplier,
+  private val schemaRevisionResolver: SchemaRevisionResolver
+) : AvroSchemaRegistry {
+  companion object {
+    const val KEY_NAME = "name"
+    const val KEY_NAMESPACE = "namespace"
+    const val KEY_CANONICAL_NAME = "canonicalName"
+    const val KEY_REVISION = "revision"
+    const val DEFAULT_GROUP = "default" // TODO: should this be configurable?
+  }
+
+  private val logger = LoggerFactory.getLogger(ApicurioAvroSchemaRegistry::class.java)
+
+  override fun register(schema: Schema): AvroSchemaWithId {
+    val content = schema.byteContent()
+    val schemaId = schemaIdSupplier.apply(schema)
+    val revision = schemaRevisionResolver.apply(schema).orElse(null)
+
+
+    val metaData: ArtifactMetaData = client.createArtifact(DEFAULT_GROUP, schemaId, ArtifactType.AVRO, IfExists.RETURN_OR_UPDATE, content)
+    logger.trace("Registered schema and received the following metadata: $metaData")
+
+    client.updateArtifactMetaData(DEFAULT_GROUP, schemaId, EditableMetaData().apply {
+      name = schema.name
+      description = schema.description()
+      properties = mapOf(
+        KEY_NAME to schema.name,
+        KEY_NAMESPACE to schema.namespace,
+        KEY_CANONICAL_NAME to schema.fullName,
+        KEY_REVISION to revision
+      )
+    })
+
+    logger.info("meta date: ${client.getArtifactMetaData(DEFAULT_GROUP, schemaId)}")
+
+    return AvroSchemaWithIdData(
+      schemaId = schemaId,
+      schema = schema,
+      revision = revision
+    )
+  }
+
+  override fun findById(schemaId: AvroSchemaId): Optional<AvroSchemaWithId> {
+
+    val schema = client.getLatestArtifact(DEFAULT_GROUP, schemaId).schema()
+    return Optional.of(
+      AvroSchemaWithIdData(
+        schemaId = schemaId,
+        schema = schema,
+        revision = schemaRevisionResolver.apply(schema).orElse(null)
+      )
+    )
+  }
+
+  override fun findByInfo(info: AvroSchemaInfo): Optional<AvroSchemaWithId> {
+    return findAllByCanonicalName(info.namespace, info.name).singleOrNull { info.revision == it.revision }
+      .let { Optional.ofNullable(it) }
+  }
+
+  override fun findAllByCanonicalName(namespace: String, name: String): List<AvroSchemaWithId> {
+    return client.listArtifactsInGroup(DEFAULT_GROUP).artifacts
+      .asSequence()
+      .filter { it.name == name }
+      .map { client.getArtifactMetaData(DEFAULT_GROUP, it.id) }
+      .filter { it.namespace().orElse("") == namespace }
+      .map { findById(it.id) }
+      .filter { it.isPresent }
+      .map { it.get() }
+      .toList()
+  }
+
+  override fun findAll(): List<AvroSchemaWithId> {
+    val artifactIds = client.listArtifactsInGroup(DEFAULT_GROUP).artifacts
+    return artifactIds.map { findById(it.id) }.filter { it.isPresent }.map { it.get() }
+  }
+
+  private fun InputStream.schema(): Schema = this.bufferedReader(Charsets.UTF_8).use {
+    val text = it.readText()
+    Schema.Parser().parse(text)
+  }
+
+  private fun ArtifactMetaData.revision(): AvroSchemaRevision? = this.properties[KEY_REVISION]
+  private fun ArtifactMetaData.namespace(): Optional<String> = Optional.ofNullable(this.properties[KEY_NAMESPACE])
+}
diff --git a/extension/registry/apicurio-rest/src/main/kotlin/AvroAdapterApicurioRest.kt b/extension/registry/apicurio-rest/src/main/kotlin/AvroAdapterApicurioRest.kt
new file mode 100644
index 00000000..a399b41b
--- /dev/null
+++ b/extension/registry/apicurio-rest/src/main/kotlin/AvroAdapterApicurioRest.kt
@@ -0,0 +1,28 @@
+package io.holixon.avro.adapter.registry.apicurio
+
+import io.apicurio.registry.rest.client.RegistryClient
+import io.apicurio.registry.rest.client.RegistryClientFactory
+import org.apache.avro.Schema
+
+/**
+ *
+ * REST endpoint (version 1.3.x): `http://$host:port/api`
+ * REST endpoint (version 2.0.x): `http://$host:port/api/registry/v2`
+ *
+ */
+object AvroAdapterApicurioRest {
+
+  @JvmOverloads
+  @JvmStatic
+  fun registryApiUrl(host: String, port: Int, https: Boolean = false) = "http${if (https) "s" else ""}://$host:$port/apis/registry/v2"
+
+  @JvmOverloads
+  @JvmStatic
+  fun registryRestClient(host: String, port: Int, https: Boolean = false): RegistryClient = registryRestClient(registryApiUrl(host, port, https))
+
+  @JvmStatic
+  fun registryRestClient(apiUrl : String): RegistryClient = RegistryClientFactory.create(apiUrl)
+
+  fun Schema.description(): String? = this.doc
+
+}
diff --git a/extension/registry/apicurio-rest/src/main/resources/.gitkeep b/extension/registry/apicurio-rest/src/main/resources/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/extension/registry/apicurio-rest/src/test/kotlin/ApicurioAvroSchemaRegistryITest.kt b/extension/registry/apicurio-rest/src/test/kotlin/ApicurioAvroSchemaRegistryITest.kt
new file mode 100644
index 00000000..40cd7d1a
--- /dev/null
+++ b/extension/registry/apicurio-rest/src/test/kotlin/ApicurioAvroSchemaRegistryITest.kt
@@ -0,0 +1,114 @@
+package io.holixon.avro.adapter.registry.apicurio
+
+import io.holixon.avro.adapter.api.type.AvroSchemaInfoData
+import io.holixon.avro.adapter.common.AvroAdapterDefault
+import io.holixon.avro.adapter.registry.apicurio.ApicurioRegistryTestContainer.Companion.EXPOSED_PORT
+import io.holixon.avro.lib.test.AvroAdapterTestLib
+import io.holixon.avro.lib.test.schema.SampleEventV4711
+import mu.KLogging
+import org.apache.avro.Schema
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.testcontainers.containers.GenericContainer
+import org.testcontainers.containers.output.Slf4jLogConsumer
+import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy
+import org.testcontainers.junit.jupiter.Container
+import org.testcontainers.junit.jupiter.Testcontainers
+
+/**
+ * Test container for apicurio tests.
+ */
+class ApicurioRegistryTestContainer : GenericContainer<ApicurioRegistryTestContainer>("apicurio/apicurio-registry-mem:2.0.0.Final") {
+  companion object {
+    const val EXPOSED_PORT = 8080
+  }
+
+  /**
+   * Delivers the REST client for Apicurio access.
+   */
+  fun restClient() = AvroAdapterApicurioRest.registryRestClient(containerIpAddress, getMappedPort(EXPOSED_PORT))
+}
+
+@Testcontainers
+internal class ApicurioAvroSchemaRegistryITest {
+  companion object : KLogging() {
+
+    @Container
+    @JvmStatic
+    val CONTAINER = ApicurioRegistryTestContainer().apply {
+      withExposedPorts(EXPOSED_PORT)
+      withLogConsumer(Slf4jLogConsumer(logger))
+      setWaitStrategy(HostPortWaitStrategy())
+    }
+  }
+
+  private val registryClient by lazy {
+    ApicurioAvroSchemaRegistry(
+      CONTAINER.restClient(),
+      AvroAdapterDefault.schemaIdSupplier,
+      AvroAdapterDefault.schemaRevisionResolver
+    )
+  }
+
+  @Test
+  internal fun `find by id`() {
+    val schema: Schema = SampleEventV4711.schema
+    val fingerprint = AvroAdapterDefault.schemaIdSupplier.apply(schema)
+
+    val created = registryClient.register(schema)
+    logger.info { "created: $created" }
+
+    val (schemaId, foundSchema, revision) = registryClient.findById(fingerprint).orElseThrow()
+
+    assertThat(schemaId).isEqualTo(fingerprint)
+    assertThat(foundSchema).isEqualTo(schema)
+    assertThat(revision).isEqualTo("4711")
+  }
+
+  @Test
+  internal fun `find by info`() {
+    val schema: Schema = AvroAdapterTestLib.schemaSampleEvent4711
+    val fingerprint = AvroAdapterDefault.schemaIdSupplier.apply(schema)
+
+    val created = registryClient.register(schema)
+    logger.info { "created: $created" }
+
+    val found = registryClient.findByInfo(
+      AvroSchemaInfoData(
+        namespace = schema.namespace,
+        name = schema.name,
+        revision = "4711"
+      )
+    )
+
+    assertThat(found).isNotEmpty
+    assertThat(found.get().schemaId).isEqualTo(fingerprint)
+
+    assertThat(
+      registryClient.findByInfo(
+        AvroSchemaInfoData(
+          namespace = schema.namespace,
+          name = schema.name,
+          revision = "4712"
+        )
+      )
+    ).isEmpty
+  }
+
+  @Test
+  internal fun `find by context and name`() {
+    val schema = SampleEventV4711.schema
+    val fingerprint = SampleEventV4711.schemaData.fingerPrint.toString()
+
+    val created = registryClient.register(schema)
+    logger.info { "created: $created" }
+
+    val found = registryClient.findAllByCanonicalName(
+      namespace = schema.namespace,
+      name = schema.name
+    )
+
+    assertThat(found).hasSize(1)
+    assertThat(found.first().schemaId).isEqualTo(fingerprint)
+  }
+}
diff --git a/extension/registry/apicurio-rest/src/test/resources/.gitkeep b/extension/registry/apicurio-rest/src/test/resources/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/lib/README.md b/lib/README.md
new file mode 100644
index 00000000..03f45228
--- /dev/null
+++ b/lib/README.md
@@ -0,0 +1,6 @@
+# Module: /lib
+
+Code/Resources of submodules beyond this module are not part of the
+release build. 
+
+They are supposed to be used for testing and example code.
diff --git a/lib/coverage-aggregate/pom.xml b/lib/coverage-aggregate/pom.xml
new file mode 100644
index 00000000..f3b9efe1
--- /dev/null
+++ b/lib/coverage-aggregate/pom.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>io.holixon.avro._</groupId>
+    <artifactId>lib</artifactId>
+    <version>0.0.1</version>
+  </parent>
+
+  <artifactId>avro-registry-adapter-lib-coverage-aggregator</artifactId>
+  <description>Aggregates coverage reports from JUnit test an I-Tests.</description>
+
+  <properties>
+    <jacoco.skip>false</jacoco.skip>
+    <deploy.skip>true</deploy.skip>
+  </properties>
+
+  <!-- Coverage aggregates works based on dependencies defined here -->
+  <dependencies>
+    <dependency>
+      <groupId>io.holixon.avro</groupId>
+      <artifactId>avro-registry-adapter-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.holixon.avro</groupId>
+      <artifactId>avro-registry-adapter-default</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.holixon.avro</groupId>
+      <artifactId>avro-registry-adapter-apicurio</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.holixon.avro._</groupId>
+      <artifactId>avro-registry-adapter-lib-test</artifactId>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.jacoco</groupId>
+        <artifactId>jacoco-maven-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>report-aggregate</id>
+            <phase>verify</phase>
+            <goals>
+              <goal>report-aggregate</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/lib/pom.xml b/lib/pom.xml
new file mode 100644
index 00000000..03fb2db3
--- /dev/null
+++ b/lib/pom.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>io.holixon.avro._</groupId>
+    <artifactId>avro-registry-adapter-parent</artifactId>
+    <version>0.0.1</version>
+    <relativePath>../parent/pom.xml</relativePath>
+  </parent>
+
+  <artifactId>lib</artifactId>
+  <packaging>pom</packaging>
+
+  <modules>
+    <module>test</module>
+    <module>coverage-aggregate</module>
+  </modules>
+</project>
diff --git a/lib/test/README.md b/lib/test/README.md
new file mode 100644
index 00000000..ea681c3b
--- /dev/null
+++ b/lib/test/README.md
@@ -0,0 +1,6 @@
+# Module: /lib/test
+
+This module provides a common test scope dependency and configuration setup,
+used by all extension sub modules and examples.
+
+This avoids redundant declaration of dependencies over and over again.
diff --git a/lib/test/pom.xml b/lib/test/pom.xml
new file mode 100644
index 00000000..150fba6b
--- /dev/null
+++ b/lib/test/pom.xml
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>io.holixon.avro._</groupId>
+    <artifactId>lib</artifactId>
+    <version>0.0.1</version>
+  </parent>
+
+  <artifactId>avro-registry-adapter-lib-test</artifactId>
+  <description>Internally used test library</description>
+
+  <properties>
+    <jacoco.skip>false</jacoco.skip>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.avro</groupId>
+      <artifactId>avro</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.jetbrains.kotlin</groupId>
+      <artifactId>kotlin-stdlib-jdk8</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.github.microutils</groupId>
+      <artifactId>kotlin-logging-jvm</artifactId>
+      <version>${kotlin-logging.version}</version>
+    </dependency>
+
+
+    <!-- all test dependencies are on the compile scope, because this is a test lib which is used on the test scope -->
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter</artifactId>
+      <scope>compile</scope>
+      <version>5.7.2</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.jetbrains.kotlin</groupId>
+      <artifactId>kotlin-test-junit5</artifactId>
+      <scope>compile</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <scope>compile</scope>
+      <version>3.10.0</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.mockito.kotlin</groupId>
+      <artifactId>mockito-kotlin</artifactId>
+      <version>3.2.0</version>
+      <scope>compile</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.testcontainers</groupId>
+      <artifactId>junit-jupiter</artifactId>
+      <scope>compile</scope>
+      <version>1.15.3</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-simple</artifactId>
+      <version>1.7.30</version>
+      <scope>compile</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.assertj</groupId>
+      <artifactId>assertj-core</artifactId>
+      <version>3.19.0</version>
+      <scope>compile</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.awaitility</groupId>
+      <artifactId>awaitility-kotlin</artifactId>
+      <version>4.1.0</version>
+      <scope>compile</scope>
+    </dependency>
+
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.avro</groupId>
+        <artifactId>avro-maven-plugin</artifactId>
+        <version>${avro.version}</version>
+        <executions>
+          <execution>
+            <phase>generate-sources</phase>
+            <goals>
+              <goal>schema</goal>
+            </goals>
+            <configuration>
+              <sourceDirectory>${project.basedir}/src/main/resources/avro/</sourceDirectory>
+              <outputDirectory>${project.build.directory}/generated-sources/avro/</outputDirectory>
+              <stringType>String</stringType>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/lib/test/src/main/kotlin/AvroAdapterTestLib.kt b/lib/test/src/main/kotlin/AvroAdapterTestLib.kt
new file mode 100644
index 00000000..66ca92a2
--- /dev/null
+++ b/lib/test/src/main/kotlin/AvroAdapterTestLib.kt
@@ -0,0 +1,30 @@
+package io.holixon.avro.lib.test
+
+import io.holixon.avro.lib.test.schema.SampleEventV4711
+import io.holixon.avro.lib.test.schema.SampleEventV4713
+import org.apache.avro.Schema
+import test.fixture.SampleEvent
+import test.fixture.SampleEventWithAdditionalFieldWithDefault
+
+/**
+ * Test support helper.
+ */
+object AvroAdapterTestLib {
+
+  val schemaSampleEvent4711 = SampleEventV4711.schema
+  val schemaSampleEvent4713 = SampleEventV4713.schema
+
+  fun loadResource(resName:String): String = {}::class.java.getResource(resName.trailingSlash())?.readText() ?: throw IllegalStateException("resource not found: $resName")
+  fun loadArvoResource(avroFileName:String): String = loadResource("/avro/$avroFileName.avsc")
+
+  private fun String.trailingSlash() = if (startsWith("/")) this else "/$this"
+
+  fun loadSchema(resName:String): Schema = Schema.Parser().parse(loadArvoResource(resName))
+
+  val sampleFoo = SampleEvent("foo")
+  val sampleFooWithAdditionalFieldWithDefault = SampleEventWithAdditionalFieldWithDefault("foo", "default value")
+  const val sampleFooHex = "[C3 01 CC 98 1F E7 56 D4 1C A5 06 66 6F 6F]"
+
+  const val sampleEventFingerprint = -6549126288393660212L
+
+}
diff --git a/lib/test/src/main/kotlin/schema/SampleEvents.kt b/lib/test/src/main/kotlin/schema/SampleEvents.kt
new file mode 100644
index 00000000..2e75221d
--- /dev/null
+++ b/lib/test/src/main/kotlin/schema/SampleEvents.kt
@@ -0,0 +1,73 @@
+package io.holixon.avro.lib.test.schema
+
+import io.holixon.avro.lib.test.AvroAdapterTestLib
+import org.apache.avro.generic.GenericData
+import org.apache.avro.generic.GenericRecord
+
+const val SCHEMA_CONTEXT = "test.fixture"
+const val SCHEMA_NAME = "SampleEvent"
+
+/**
+ * Test schema provider.
+ */
+object SampleEventV4711 : TestSchemaDataProvider {
+  const val REVISION = "4711"
+
+  override val schemaJson by lazy { AvroAdapterTestLib.loadArvoResource("$SCHEMA_CONTEXT.$SCHEMA_NAME-v$REVISION") }
+}
+
+/**
+ * Test schema provider.
+ */
+object SampleEventV4712 : TestSchemaDataProvider {
+  const val REVISION = "4712"
+
+  override val schemaJson: String = """{
+      "type": "record",
+      "namespace": "$SCHEMA_CONTEXT",
+      "name": "$SCHEMA_NAME",
+      "revision": "$REVISION",
+      "doc": "used for testing, has optional field",
+      "fields": [
+        {
+          "name": "value",
+          "doc": "this is the first value, it is required",
+          "type": {
+            "type": "string",
+            "avro.java.string": "String"
+          }
+        },
+        {
+          "name": "anotherValue",
+          "doc": "this is the second value, it is optional",
+          "type": [
+            {
+              "type": "string",
+              "avro.java.string": "String"
+            },
+            "null"
+          ],
+          "default": "null"
+        }
+      ],
+      "__comment": "some additional comment"
+    }"""
+
+  fun create(value: String, anotherValue:String?= null) : GenericRecord = GenericData.Record(schema).apply {
+    put("value", value)
+    anotherValue?.also { put("anotherValue", anotherValue) }
+  }
+}
+
+/**
+ * Test provider.
+ */
+object SampleEventV4713 : TestSchemaDataProvider {
+  const val REVISION = "4713"
+  const val SUFFIX = "WithAdditionalFieldWithDefault"
+
+  fun addSuffix(fqn: String) = fqn + SUFFIX
+  fun removeSuffix(fqn:String) = fqn.removeSuffix(SUFFIX)
+
+  override val schemaJson by lazy { AvroAdapterTestLib.loadArvoResource("$SCHEMA_CONTEXT.$SCHEMA_NAME-v$REVISION") }
+}
diff --git a/lib/test/src/main/kotlin/schema/TestSchemaData.kt b/lib/test/src/main/kotlin/schema/TestSchemaData.kt
new file mode 100644
index 00000000..c20b1838
--- /dev/null
+++ b/lib/test/src/main/kotlin/schema/TestSchemaData.kt
@@ -0,0 +1,36 @@
+package io.holixon.avro.lib.test.schema
+
+import org.apache.avro.Schema
+import org.apache.avro.SchemaNormalization
+
+data class TestSchemaData(
+  val schema: Schema,
+  val name: String,
+  val namespace: String,
+  val doc: String,
+  val fingerPrint: Long,
+  val revision: String?
+) {
+  constructor(schema: Schema) : this(
+    schema = schema,
+    name = schema.name,
+    namespace = schema.namespace,
+    doc = schema.doc,
+    fingerPrint = SchemaNormalization.parsingFingerprint64(schema),
+    revision = schema.getObjectProp("revision") as String?
+  )
+
+  constructor(schemaJson: String) : this(Schema.Parser().parse(schemaJson))
+}
+
+interface TestSchemaDataProvider {
+  val schemaJson: String
+
+  val schemaData: TestSchemaData
+    get() = TestSchemaData(schemaJson)
+
+  val schema: Schema
+    get() = schemaData.schema
+
+  fun toJson() = schema.toString(true)
+}
diff --git a/lib/test/src/main/resources/avro/test.fixture.SampleEvent-v4711.avsc b/lib/test/src/main/resources/avro/test.fixture.SampleEvent-v4711.avsc
new file mode 100644
index 00000000..dd1c2552
--- /dev/null
+++ b/lib/test/src/main/resources/avro/test.fixture.SampleEvent-v4711.avsc
@@ -0,0 +1,17 @@
+{
+  "type": "record",
+  "namespace": "test.fixture",
+  "name": "SampleEvent",
+  "doc": "a sample event for testing",
+  "revision": "4711",
+  "fields": [
+    {
+      "name": "value",
+      "type": {
+        "type": "string",
+        "avro.java.string": "String"
+      }
+    }
+  ],
+  "__comment": "additional comment"
+}
diff --git a/lib/test/src/main/resources/avro/test.fixture.SampleEvent-v4713.avsc b/lib/test/src/main/resources/avro/test.fixture.SampleEvent-v4713.avsc
new file mode 100644
index 00000000..e8f67cae
--- /dev/null
+++ b/lib/test/src/main/resources/avro/test.fixture.SampleEvent-v4713.avsc
@@ -0,0 +1,25 @@
+{
+  "type": "record",
+  "namespace": "test.fixture",
+  "name": "SampleEventWithAdditionalFieldWithDefault",
+  "doc": "a sample event for testing",
+  "revision": "4713",
+  "fields": [
+    {
+      "name": "value",
+      "type": {
+        "type": "string",
+        "avro.java.string": "String"
+      }
+    },
+    {
+      "name": "otherValue",
+      "type": {
+        "type": "string",
+        "avro.java.string": "String"
+      },
+      "default": "default value"
+    }
+  ],
+  "__comment": "additional comment"
+}
diff --git a/lib/test/src/main/resources/simplelogger.properties b/lib/test/src/main/resources/simplelogger.properties
new file mode 100644
index 00000000..c80d8479
--- /dev/null
+++ b/lib/test/src/main/resources/simplelogger.properties
@@ -0,0 +1,6 @@
+org.slf4j.simpleLogger.defaultLogLevel  = info
+
+org.slf4j.simpleLogger.showShortLogName = true
+org.slf4j.simpleLogger.levelInBrackets  = true
+
+#org.slf4j.simpleLogger.log.org.camunda.bpm.engine.persistence = warn
diff --git a/lib/test/src/test/kotlin/AvroAdapterTestLibTest.kt b/lib/test/src/test/kotlin/AvroAdapterTestLibTest.kt
new file mode 100644
index 00000000..f6f267e9
--- /dev/null
+++ b/lib/test/src/test/kotlin/AvroAdapterTestLibTest.kt
@@ -0,0 +1,45 @@
+package io.holixon.avro.lib.test
+
+import io.holixon.avro.lib.test.schema.SampleEventV4711
+import io.holixon.avro.lib.test.schema.SampleEventV4712
+import org.apache.avro.generic.GenericData
+import org.apache.avro.generic.GenericRecord
+import org.apache.avro.specific.SpecificData
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import test.fixture.SampleEvent
+
+internal class AvroAdapterTestLibTest {
+
+  @Test
+  internal fun `schema matches`() {
+    assertThat(SampleEvent.`SCHEMA$`).isEqualTo(SampleEventV4711.schema)
+  }
+
+  @Test
+  internal fun `create specific from generic`() {
+    val generic = GenericData.Record(SampleEvent.`SCHEMA$`).apply {
+      put("value", "foo")
+    }
+
+    val specific = SpecificData.get().deepCopy(SampleEvent.`SCHEMA$`, generic) as SampleEvent
+
+    assertThat(specific.value).isEqualTo("foo")
+  }
+
+  @Test
+  internal fun `generic record from schema 4712`() {
+
+    println(SampleEventV4712.create("foo"))
+    println(SampleEventV4712.create(value = "foo", anotherValue = "bar"))
+    println(SampleEventV4712.create(value = "foo", anotherValue = null))
+
+    val record4712: GenericRecord = SampleEventV4712.create("foo")
+
+    println("rec: $record4712")
+
+    val sd4711 = SpecificData.get().deepCopy(SampleEventV4711.schema, record4712) as SampleEvent
+
+    println("spec: $sd4711")
+  }
+}
diff --git a/lib/test/src/test/kotlin/schema/SampleEventV4711Test.kt b/lib/test/src/test/kotlin/schema/SampleEventV4711Test.kt
new file mode 100644
index 00000000..7df01f52
--- /dev/null
+++ b/lib/test/src/test/kotlin/schema/SampleEventV4711Test.kt
@@ -0,0 +1,16 @@
+package io.holixon.avro.lib.test.schema
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+
+internal class SampleEventV4711Test {
+
+  @Test
+  internal fun `load from resource`() {
+    val schemaData = SampleEventV4711.schemaData
+
+    assertThat(schemaData.name).isEqualTo("SampleEvent")
+    assertThat(schemaData.revision).isEqualTo("4711")
+    assertThat(schemaData.doc).isEqualTo("a sample event for testing")
+  }
+}
diff --git a/lib/test/src/test/kotlin/schema/SampleEventV4712Test.kt b/lib/test/src/test/kotlin/schema/SampleEventV4712Test.kt
new file mode 100644
index 00000000..ea03712b
--- /dev/null
+++ b/lib/test/src/test/kotlin/schema/SampleEventV4712Test.kt
@@ -0,0 +1,16 @@
+package io.holixon.avro.lib.test.schema
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+
+internal class SampleEventV4712Test {
+
+  @Test
+  internal fun `parse schema data`() {
+    val schemaData = SampleEventV4712.schemaData
+
+    assertThat(schemaData.name).isEqualTo("SampleEvent")
+
+    assertThat(schemaData.schema.fields.map { it.name() }).containsExactlyInAnyOrder("value", "anotherValue")
+  }
+}
diff --git a/mvnw b/mvnw
new file mode 100755
index 00000000..41c0f0c2
--- /dev/null
+++ b/mvnw
@@ -0,0 +1,310 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+#   JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+#   M2_HOME - location of maven2's installed home dir
+#   MAVEN_OPTS - parameters passed to the Java VM when running Maven
+#     e.g. to debug Maven itself, use
+#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+  if [ -f /etc/mavenrc ] ; then
+    . /etc/mavenrc
+  fi
+
+  if [ -f "$HOME/.mavenrc" ] ; then
+    . "$HOME/.mavenrc"
+  fi
+
+fi
+
+# OS specific support.  $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+  CYGWIN*) cygwin=true ;;
+  MINGW*) mingw=true;;
+  Darwin*) darwin=true
+    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+    if [ -z "$JAVA_HOME" ]; then
+      if [ -x "/usr/libexec/java_home" ]; then
+        export JAVA_HOME="`/usr/libexec/java_home`"
+      else
+        export JAVA_HOME="/Library/Java/Home"
+      fi
+    fi
+    ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+  if [ -r /etc/gentoo-release ] ; then
+    JAVA_HOME=`java-config --jre-home`
+  fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+  ## resolve links - $0 may be a link to maven's home
+  PRG="$0"
+
+  # need this for relative symlinks
+  while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+      PRG="$link"
+    else
+      PRG="`dirname "$PRG"`/$link"
+    fi
+  done
+
+  saveddir=`pwd`
+
+  M2_HOME=`dirname "$PRG"`/..
+
+  # make it fully qualified
+  M2_HOME=`cd "$M2_HOME" && pwd`
+
+  cd "$saveddir"
+  # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME=`cygpath --unix "$M2_HOME"`
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+  [ -n "$CLASSPATH" ] &&
+    CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME="`(cd "$M2_HOME"; pwd)`"
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+  javaExecutable="`which javac`"
+  if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+    # readlink(1) is not available as standard on Solaris 10.
+    readLink=`which readlink`
+    if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+      if $darwin ; then
+        javaHome="`dirname \"$javaExecutable\"`"
+        javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+      else
+        javaExecutable="`readlink -f \"$javaExecutable\"`"
+      fi
+      javaHome="`dirname \"$javaExecutable\"`"
+      javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+      JAVA_HOME="$javaHome"
+      export JAVA_HOME
+    fi
+  fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+  if [ -n "$JAVA_HOME"  ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+      # IBM's JDK on AIX uses strange locations for the executables
+      JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+      JAVACMD="$JAVA_HOME/bin/java"
+    fi
+  else
+    JAVACMD="`which java`"
+  fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+  echo "Error: JAVA_HOME is not defined correctly." >&2
+  echo "  We cannot execute $JAVACMD" >&2
+  exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+  echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+  if [ -z "$1" ]
+  then
+    echo "Path not specified to find_maven_basedir"
+    return 1
+  fi
+
+  basedir="$1"
+  wdir="$1"
+  while [ "$wdir" != '/' ] ; do
+    if [ -d "$wdir"/.mvn ] ; then
+      basedir=$wdir
+      break
+    fi
+    # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+    if [ -d "${wdir}" ]; then
+      wdir=`cd "$wdir/.."; pwd`
+    fi
+    # end of workaround
+  done
+  echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+  if [ -f "$1" ]; then
+    echo "$(tr -s '\n' ' ' < "$1")"
+  fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+  exit 1;
+fi
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Found .mvn/wrapper/maven-wrapper.jar"
+    fi
+else
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
+    fi
+    if [ -n "$MVNW_REPOURL" ]; then
+      jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+    else
+      jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+    fi
+    while IFS="=" read key value; do
+      case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+      esac
+    done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Downloading from: $jarUrl"
+    fi
+    wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+    if $cygwin; then
+      wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
+    fi
+
+    if command -v wget > /dev/null; then
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Found wget ... using wget"
+        fi
+        if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+            wget "$jarUrl" -O "$wrapperJarPath"
+        else
+            wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
+        fi
+    elif command -v curl > /dev/null; then
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Found curl ... using curl"
+        fi
+        if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+            curl -o "$wrapperJarPath" "$jarUrl" -f
+        else
+            curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
+        fi
+
+    else
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Falling back to using Java to download"
+        fi
+        javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+        # For Cygwin, switch paths to Windows format before running javac
+        if $cygwin; then
+          javaClass=`cygpath --path --windows "$javaClass"`
+        fi
+        if [ -e "$javaClass" ]; then
+            if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+                if [ "$MVNW_VERBOSE" = true ]; then
+                  echo " - Compiling MavenWrapperDownloader.java ..."
+                fi
+                # Compiling the Java class
+                ("$JAVA_HOME/bin/javac" "$javaClass")
+            fi
+            if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+                # Running the downloader
+                if [ "$MVNW_VERBOSE" = true ]; then
+                  echo " - Running MavenWrapperDownloader.java ..."
+                fi
+                ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+            fi
+        fi
+    fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+if [ "$MVNW_VERBOSE" = true ]; then
+  echo $MAVEN_PROJECTBASEDIR
+fi
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME=`cygpath --path --windows "$M2_HOME"`
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+  [ -n "$CLASSPATH" ] &&
+    CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+  [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+    MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+  $MAVEN_OPTS \
+  -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+  "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/mvnw.cmd b/mvnw.cmd
new file mode 100644
index 00000000..86115719
--- /dev/null
+++ b/mvnw.cmd
@@ -0,0 +1,182 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements.  See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership.  The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License.  You may obtain a copy of the License at
+@REM
+@REM    http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied.  See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM     e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on"  echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+
+FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+    IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Found %WRAPPER_JAR%
+    )
+) else (
+    if not "%MVNW_REPOURL%" == "" (
+        SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+    )
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Couldn't find %WRAPPER_JAR%, downloading it ...
+        echo Downloading from: %DOWNLOAD_URL%
+    )
+
+    powershell -Command "&{"^
+		"$webclient = new-object System.Net.WebClient;"^
+		"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+		"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+		"}"^
+		"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
+		"}"
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Finished downloading %WRAPPER_JAR%
+    )
+)
+@REM End of extension
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%
diff --git a/parent/pom.xml b/parent/pom.xml
new file mode 100644
index 00000000..2263d0ba
--- /dev/null
+++ b/parent/pom.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>io.holixon.avro._</groupId>
+    <artifactId>avro-registry-adapter</artifactId>
+    <version>0.0.1</version>
+  </parent>
+
+  <artifactId>avro-registry-adapter-parent</artifactId>
+  <packaging>pom</packaging>
+
+  <properties>
+    <avro.version>1.10.2</avro.version>
+    <kotlin-logging.version>2.0.6</kotlin-logging.version>
+    <apicurio.version>2.0.0.Final</apicurio.version>
+  </properties>
+
+  <dependencyManagement>
+    <dependencies>
+      <!-- KOTLIN -->
+      <dependency>
+        <groupId>org.jetbrains.kotlin</groupId>
+        <artifactId>kotlin-bom</artifactId>
+        <version>${kotlin.version}</version>
+        <scope>import</scope>
+        <type>pom</type>
+      </dependency>
+
+      <dependency>
+        <groupId>io.holixon.avro</groupId>
+        <artifactId>avro-registry-adapter-bom</artifactId>
+        <version>${project.version}</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+
+      <dependency>
+        <groupId>io.holixon.avro._</groupId>
+        <artifactId>avro-registry-adapter-lib-test</artifactId>
+        <version>${project.version}</version>
+        <scope>test</scope>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+
+</project>
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 00000000..75f09551
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,586 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>io.holixon.avro._</groupId>
+  <artifactId>avro-registry-adapter</artifactId>
+  <version>0.0.1</version>
+  <url>https://github.com/holixon/avro-registry-adapter/</url>
+  <packaging>pom</packaging>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+    <java.version>11.0</java.version>
+
+    <kotlin.version>1.5.10</kotlin.version>
+
+    <pattern.class.itest>**/*ITest.*</pattern.class.itest>
+    <pattern.package.itest>**/itest/**/*.*</pattern.package.itest>
+
+    <!-- Skip instrumentalization by default -->
+    <jacoco.skip>true</jacoco.skip>
+  </properties>
+
+  <modules>
+    <module>parent</module>
+    <module>lib</module>
+    <module>extension</module>
+    <module>bom</module>
+    <!-- More modules in profile examples  -->
+  </modules>
+
+  <build>
+    <defaultGoal>clean jacoco:prepare-agent package</defaultGoal>
+    <resources>
+      <!-- ignore .gitkeep marker files -->
+      <resource>
+        <directory>src/main/resources</directory>
+        <excludes>
+          <exclude>.gitkeep</exclude>
+        </excludes>
+      </resource>
+    </resources>
+
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <!-- cleaning -->
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-clean-plugin</artifactId>
+          <version>3.1.0</version>
+        </plugin>
+        
+        <!-- Coverage metering -->
+        <plugin>
+          <groupId>org.jacoco</groupId>
+          <artifactId>jacoco-maven-plugin</artifactId>
+          <version>0.8.7</version>
+          <executions>
+            <execution>
+              <id>pre-unit-test</id>
+              <goals>
+                <goal>prepare-agent</goal>
+              </goals>
+            </execution>
+            <execution>
+              <id>pre-integration-test</id>
+              <goals>
+                <goal>prepare-agent-integration</goal>
+              </goals>
+            </execution>
+          </executions>
+        </plugin>
+
+        <plugin>
+          <!-- java compiler -->
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-compiler-plugin</artifactId>
+          <version>3.8.1</version>
+          <configuration>
+            <encoding>${project.build.sourceEncoding}</encoding>
+            <release>11</release>
+          </configuration>
+          <executions>
+            <!-- Replacing default-compile as it is treated specially by maven -->
+            <execution>
+              <id>default-compile</id>
+              <phase>none</phase>
+            </execution>
+            <!-- Replacing default-testCompile as it is treated specially by maven -->
+            <execution>
+              <id>default-testCompile</id>
+              <phase>none</phase>
+            </execution>
+            <execution>
+              <id>java-compile</id>
+              <phase>compile</phase>
+              <goals>
+                <goal>compile</goal>
+              </goals>
+            </execution>
+            <execution>
+              <id>java-test-compile</id>
+              <phase>test-compile</phase>
+              <goals>
+                <goal>testCompile</goal>
+              </goals>
+            </execution>
+          </executions>
+        </plugin>
+
+        <plugin>
+          <!-- kotlin compiler -->
+          <groupId>org.jetbrains.kotlin</groupId>
+          <artifactId>kotlin-maven-plugin</artifactId>
+          <version>${kotlin.version}</version>
+          <configuration>
+            <jvmTarget>11</jvmTarget>
+            <compilerPlugins>
+              <plugin>spring</plugin>
+              <plugin>no-arg</plugin>
+              <plugin>all-open</plugin>
+            </compilerPlugins>
+            <pluginOptions>
+              <!-- Each annotation is placed on its own line -->
+              <option>all-open:annotation=com.tngtech.jgiven.integration.spring.JGivenStage</option>
+            </pluginOptions>
+          </configuration>
+
+          <executions>
+            <execution>
+              <id>compile</id>
+              <phase>compile</phase>
+              <goals>
+                <goal>compile</goal>
+              </goals>
+              <configuration>
+                <sourceDirs>
+                  <sourceDir>${project.basedir}/src/main/kotlin</sourceDir>
+                </sourceDirs>
+              </configuration>
+            </execution>
+            <execution>
+              <id>test-compile</id>
+              <phase>test-compile</phase>
+              <goals>
+                <goal>test-compile</goal>
+              </goals>
+              <configuration>
+                <sourceDirs>
+                  <sourceDir>${project.basedir}/src/test/kotlin</sourceDir>
+                  <sourceDir>${project.basedir}/src/test/java</sourceDir>
+                </sourceDirs>
+              </configuration>
+            </execution>
+          </executions>
+
+          <dependencies>
+            <dependency>
+              <groupId>org.jetbrains.kotlin</groupId>
+              <artifactId>kotlin-maven-allopen</artifactId>
+              <version>${kotlin.version}</version>
+            </dependency>
+            <dependency>
+              <groupId>org.jetbrains.kotlin</groupId>
+              <artifactId>kotlin-maven-noarg</artifactId>
+              <version>${kotlin.version}</version>
+            </dependency>
+          </dependencies>
+        </plugin>
+
+        <plugin>
+          <!-- resources -->
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-resources-plugin</artifactId>
+          <version>3.2.0</version>
+          <configuration>
+            <encoding>UTF-8</encoding>
+          </configuration>
+        </plugin>
+
+        <!-- Unit Tests -->
+        <plugin>
+          <artifactId>maven-surefire-plugin</artifactId>
+          <version>3.0.0-M5</version>
+          <configuration>
+            <useSystemClassLoader>false</useSystemClassLoader>
+            <shutdown>kill</shutdown>
+            <runOrder>random</runOrder>
+            <excludes>
+              <exclude>${pattern.class.itest}</exclude>
+              <exclude>${pattern.package.itest}</exclude>
+            </excludes>
+            <!-- Sets the VM argument line used when unit tests are run. -->
+            <!-- prevent the annoying ForkedBooter process from stealing window focus on Mac OS -->
+            <!-- suppress UnresolvedMavenProperty -->
+            <argLine>--illegal-access=permit -Djava.awt.headless=true ${argLine} -XX:+StartAttachListener -Xmx1024m
+            </argLine>
+            <systemPropertyVariables>
+              <jgiven.report.dir>${project.build.directory}/jgiven-reports</jgiven.report.dir>
+            </systemPropertyVariables>
+          </configuration>
+        </plugin>
+
+        <!-- ITests -->
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-failsafe-plugin</artifactId>
+          <version>3.0.0-M5</version>
+          <executions>
+            <execution>
+              <goals>
+                <goal>integration-test</goal>
+              </goals>
+            </execution>
+            <execution>
+              <id>verify results</id>
+              <phase>verify</phase>
+              <goals>
+                <goal>verify</goal>
+              </goals>
+            </execution>
+          </executions>
+          <configuration>
+            <includes>
+              <include>${pattern.class.itest}</include>
+              <include>${pattern.package.itest}</include>
+            </includes>
+            <!-- Sets the VM argument line used when unit tests are run. -->
+            <!-- prevent the annoying ForkedBooter process from stealing window focus on Mac OS -->
+            <!-- suppress UnresolvedMavenProperty -->
+            <argLine>-Djava.awt.headless=true --illegal-access=permit ${argLine} -XX:+StartAttachListener</argLine>
+            <runOrder>random</runOrder>
+          </configuration>
+        </plugin>
+
+        <!-- javadoc for kotlin -->
+        <plugin>
+          <groupId>org.jetbrains.dokka</groupId>
+          <artifactId>dokka-maven-plugin</artifactId>
+          <version>1.4.32</version>
+          <executions>
+            <execution>
+              <phase>package</phase>
+              <id>attach-javadocs</id>
+              <goals>
+                <goal>javadocJar</goal>
+              </goals>
+            </execution>
+          </executions>
+          <configuration>
+            <jdkVersion>11</jdkVersion>
+            <reportUndocumented>false</reportUndocumented>
+            <skipEmptyPackages>true</skipEmptyPackages>
+          </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-javadoc-plugin</artifactId>
+          <version>3.3.0</version>
+          <configuration>
+            <failOnError>false</failOnError>
+            <doclint>none</doclint>
+          </configuration>
+          <executions>
+            <execution>
+              <id>attach-javadocs</id>
+              <goals>
+                <goal>jar</goal>
+              </goals>
+            </execution>
+          </executions>
+        </plugin>
+
+        <!-- source from kotlin -->
+        <plugin>
+          <groupId>org.codehaus.mojo</groupId>
+          <artifactId>build-helper-maven-plugin</artifactId>
+          <version>3.2.0</version>
+          <executions>
+            <execution>
+              <phase>generate-sources</phase>
+              <goals>
+                <goal>add-source</goal>
+              </goals>
+              <configuration>
+                <sources>
+                  <source>${project.basedir}/src/main/kotlin</source>
+                </sources>
+              </configuration>
+            </execution>
+          </executions>
+        </plugin>
+
+        <!-- attach sources -->
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-source-plugin</artifactId>
+          <version>3.2.1</version>
+          <executions>
+            <execution>
+              <id>attach-sources</id>
+              <phase>package</phase>
+              <goals>
+                <goal>jar</goal>
+              </goals>
+              <configuration>
+                <attach>true</attach>
+                <forceCreation>true</forceCreation>
+              </configuration>
+            </execution>
+          </executions>
+        </plugin>
+
+        <!-- gitflow -->
+        <plugin>
+          <groupId>com.amashchenko.maven.plugin</groupId>
+          <artifactId>gitflow-maven-plugin</artifactId>
+          <version>1.16.0</version>
+          <configuration>
+            <gitFlowConfig>
+              <productionBranch>main</productionBranch>
+              <developmentBranch>develop</developmentBranch>
+              <featureBranchPrefix>feature/</featureBranchPrefix>
+              <releaseBranchPrefix>release/</releaseBranchPrefix>
+              <hotfixBranchPrefix>hotfix/</hotfixBranchPrefix>
+              <supportBranchPrefix>support/</supportBranchPrefix>
+              <origin>origin</origin>
+            </gitFlowConfig>
+            <useSnapshotInHotfix>true</useSnapshotInHotfix>
+            <useSnapshotInRelease>true</useSnapshotInRelease>
+            <keepBranch>false</keepBranch>
+            <pushRemote>true</pushRemote>
+          </configuration>
+        </plugin>
+
+        <!-- To sign the artifacts -->
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-gpg-plugin</artifactId>
+          <version>3.0.1</version>
+          <configuration>
+            <gpgArguments>
+              <arg>--batch</arg>
+              <arg>--yes</arg>
+              <arg>--pinentry-mode</arg>
+              <arg>loopback</arg>
+            </gpgArguments>
+          </configuration>
+          <executions>
+            <execution>
+              <id>sign-artifacts</id>
+              <phase>verify</phase>
+              <goals>
+                <goal>sign</goal>
+              </goals>
+            </execution>
+          </executions>
+        </plugin>
+
+        <!-- Deploy -->
+        <plugin>
+          <artifactId>maven-deploy-plugin</artifactId>
+          <version>3.0.0-M1</version>
+          <configuration>
+            <skip>true</skip>
+          </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.sonatype.plugins</groupId>
+          <artifactId>nexus-staging-maven-plugin</artifactId>
+          <version>1.6.8</version>
+          <configuration>
+            <autoReleaseAfterClose>true</autoReleaseAfterClose>
+            <serverId>ossrh</serverId>
+            <nexusUrl>https://oss.sonatype.org/</nexusUrl>
+          </configuration>
+          <executions>
+            <execution>
+              <id>default-deploy</id>
+              <phase>deploy</phase>
+              <goals>
+                <goal>deploy</goal>
+              </goals>
+            </execution>
+          </executions>
+        </plugin>
+
+        <!-- Install -->
+        <plugin>
+          <artifactId>maven-install-plugin</artifactId>
+          <version>2.5.2</version>
+        </plugin>
+
+        <!-- Enforce -->
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-enforcer-plugin</artifactId>
+          <version>3.0.0-M3</version>
+          <executions>
+            <execution>
+              <id>enforce-maven</id>
+              <goals>
+                <goal>enforce</goal>
+              </goals>
+              <configuration>
+                <rules>
+                  <requireMavenVersion>
+                    <version>3.6.0</version>
+                  </requireMavenVersion>
+                  <requireJavaVersion>
+                    <version>11</version>
+                  </requireJavaVersion>
+                </rules>
+              </configuration>
+            </execution>
+          </executions>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+
+    <plugins>
+      <plugin>
+        <groupId>org.jacoco</groupId>
+        <artifactId>jacoco-maven-plugin</artifactId>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-enforcer-plugin</artifactId>
+      </plugin>
+
+      <plugin>
+        <groupId>org.jetbrains.kotlin</groupId>
+        <artifactId>kotlin-maven-plugin</artifactId>
+      </plugin>
+    </plugins>
+
+  </build>
+
+  <profiles>
+    <profile>
+      <id>itest</id>
+      <activation>
+        <activeByDefault>false</activeByDefault>
+      </activation>
+      <build>
+        <defaultGoal>verify</defaultGoal>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-surefire-plugin</artifactId>
+            <configuration>
+              <!-- Skip Unit test execution on ITest profile run -->
+              <skipTests>true</skipTests>
+            </configuration>
+          </plugin>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-failsafe-plugin</artifactId>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+
+    <!--
+      Profile creating all artifacts: JARs, POMs, Sources, JavaDoc and all signatures.
+    -->
+    <profile>
+      <id>release</id>
+      <activation>
+        <property>
+          <name>release</name>
+        </property>
+      </activation>
+      <build>
+        <plugins>
+          <!-- Kdoc / JavaDoc -->
+          <plugin>
+            <groupId>org.jetbrains.dokka</groupId>
+            <artifactId>dokka-maven-plugin</artifactId>
+          </plugin>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-javadoc-plugin</artifactId>
+          </plugin>
+          <!-- Sources for Java and Kotlin -->
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-source-plugin</artifactId>
+          </plugin>
+          <plugin>
+            <groupId>org.codehaus.mojo</groupId>
+            <artifactId>build-helper-maven-plugin</artifactId>
+          </plugin>
+          <!-- Sign it all -->
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-gpg-plugin</artifactId>
+          </plugin>
+          <!-- Deploy it all -->
+          <plugin>
+            <groupId>org.sonatype.plugins</groupId>
+            <artifactId>nexus-staging-maven-plugin</artifactId>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+
+    <profile>
+      <!--
+        Profile for building examples
+      -->
+      <id>examples</id>
+      <activation>
+        <property>
+          <name>!skipExamples</name>
+        </property>
+      </activation>
+      <modules>
+        <module>examples</module>
+      </modules>
+    </profile>
+  </profiles>
+
+  <!-- Defaults to make OSS Sonatype happy -->
+  <name>${project.artifactId}</name>
+  <description>Avro Registry Adapter</description>
+
+  <licenses>
+    <license>
+      <name>The Apache Software License, Version 2.0</name>
+      <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
+    </license>
+  </licenses>
+
+  <scm>
+    <connection>scm:git:git@github.com:holixon/avro-registry-adapter.git</connection>
+    <url>scm:git:git@github.com:holixon/avro-registry-adapter.git</url>
+    <developerConnection>scm:git:git@github.com:holixon/avro-registry-adapter.git</developerConnection>
+    <tag>HEAD</tag>
+  </scm>
+
+  <distributionManagement>
+    <site>
+      <id>README</id>
+      <url>https://github.com/holixon/avro-registry-adapter</url>
+    </site>
+    <snapshotRepository>
+      <id>ossrh</id>
+      <url>https://oss.sonatype.org/content/repositories/snapshots</url>
+    </snapshotRepository>
+    <repository>
+      <id>ossrh</id>
+      <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
+    </repository>
+  </distributionManagement>
+
+  <developers>
+    <developer>
+      <id>jangalinski</id>
+      <name>Jan Galinski</name>
+      <roles>
+        <role>Product Owner</role>
+        <role>Developer</role>
+      </roles>
+      <organization>Holisticon AG</organization>
+      <organizationUrl>https://holisticon.de</organizationUrl>
+    </developer>
+    <developer>
+      <id>zambrovski</id>
+      <name>Simon Zambrovski</name>
+      <roles>
+        <role>Product Owner</role>
+        <role>Developer</role>
+      </roles>
+      <organization>Holisticon AG</organization>
+      <organizationUrl>https://holisticon.de</organizationUrl>
+    </developer>
+
+  </developers>
+
+</project>